diff --git a/README.md b/README.md index eb4113a..7f97632 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,9 @@ Auth::logout(); For a streamlined approach to authenticating customers in TastyIgniter, you can use the `\Igniter\User\Actions\LoginUser` action class. This class mirrors the authentication process used by the default login form. It also dispatches two key events — `igniter.user.beforeAuthenticate` and `igniter.user.login` — which can be used to hook into the login process for custom behavior or integrations. ```php -use Igniter\User\Actions\LoginUser; +use Igniter\User\Actions\LoginCustomer; -$loginUser = new LoginUser($credentials, $remember); +$loginUser = new LoginCustomer($credentials, $remember); $loginUser->handle(); ``` @@ -149,7 +149,7 @@ AdminAuth::getProvider()->register($staffData); For a streamlined approach to registering customers in TastyIgniter, you can use the `\Igniter\User\Actions\RegisterUser` action class. This class mirrors the registration process used by the default registration form. It also dispatches two key events — `igniter.user.beforeRegister` and `igniter.user.register` — which can be used to hook into the registration process for custom behavior or integrations. ```php -use Igniter\User\Actions\RegisterUser; +use Igniter\User\Actions\RegisterCustomer; $data = [ 'first_name' => 'John', @@ -158,7 +158,7 @@ $data = [ 'password' => 'password', ]; -$registerUser = new RegisterUser(); +$registerUser = new RegisterCustomer(); $customer = $registerUser->handle($data); if ($customer->is_activated) { @@ -175,9 +175,9 @@ if ($customer->is_activated) { The `activate` method can be used to activate a customer account. ```php -use Igniter\User\Actions\RegisterUser; +use Igniter\User\Actions\RegisterCustomer; -$registerUser = new RegisterUser(); +$registerUser = new RegisterCustomer(); $registerUser->activate(); $registerUser->sendRegisteredMail(['account_login_link' => page_url('account.login')]); diff --git a/composer.json b/composer.json index 2d860b2..ad34f04 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "require": { "spatie/laravel-activitylog": "^4.8", - "tastyigniter/core": "^v4.0@beta" + "tastyigniter/core": "^v4.0@beta || ^v4.0@dev" }, "require-dev": { "laravel/pint": "^1.2", @@ -55,5 +55,5 @@ }, "sort-packages": true }, - "minimum-stability": "beta" + "minimum-stability": "dev" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ad055e2..215aae8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,6 +12,9 @@ + + + diff --git a/src/Actions/LoginUser.php b/src/Actions/LoginCustomer.php similarity index 65% rename from src/Actions/LoginUser.php rename to src/Actions/LoginCustomer.php index c7ecc85..19dc2d9 100644 --- a/src/Actions/LoginUser.php +++ b/src/Actions/LoginCustomer.php @@ -5,21 +5,22 @@ use Igniter\Flame\Exception\FlashException; use Igniter\User\Facades\Auth; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Session; -class LoginUser +class LoginCustomer { public function __construct(public array $credentials, public bool $remember = true) {} public function handle() { - Event::fire('igniter.user.beforeAuthenticate', [$this, $this->credentials]); + Event::dispatch('igniter.user.beforeAuthenticate', [$this, $this->credentials]); if (!Auth::attempt($this->credentials, $this->remember)) { throw new FlashException(lang('igniter.user::default.login.alert_invalid_login')); } - session()->regenerate(); + Session::regenerate(); - Event::fire('igniter.user.login', [$this], true); + Event::dispatch('igniter.user.login', [$this], true); } } diff --git a/src/Actions/LogoutUser.php b/src/Actions/LogoutCustomer.php similarity index 68% rename from src/Actions/LogoutUser.php rename to src/Actions/LogoutCustomer.php index 5a0d1ad..e4df810 100644 --- a/src/Actions/LogoutUser.php +++ b/src/Actions/LogoutCustomer.php @@ -4,8 +4,9 @@ use Igniter\User\Facades\Auth; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Session; -class LogoutUser +class LogoutCustomer { public function handle() { @@ -16,12 +17,12 @@ public function handle() } else { Auth::logout(); - session()->invalidate(); + Session::invalidate(); - session()->regenerateToken(); + Session::regenerateToken(); if ($user) { - Event::fire('igniter.user.logout', [$user]); + Event::dispatch('igniter.user.logout', [$user]); } } diff --git a/src/Actions/RegisterUser.php b/src/Actions/RegisterCustomer.php similarity index 69% rename from src/Actions/RegisterUser.php rename to src/Actions/RegisterCustomer.php index ec19bde..04083e2 100644 --- a/src/Actions/RegisterUser.php +++ b/src/Actions/RegisterCustomer.php @@ -8,13 +8,13 @@ use Igniter\User\Models\CustomerGroup; use Illuminate\Support\Facades\Event; -class RegisterUser +class RegisterCustomer { public Customer $customer; public function handle(array $data = []): Customer { - Event::fire('igniter.user.beforeRegister', [&$data]); + Event::dispatch('igniter.user.beforeRegister', [&$data]); $customerGroup = CustomerGroup::getDefault(); $data['customer_group_id'] = $customerGroup?->getKey(); @@ -24,7 +24,7 @@ public function handle(array $data = []): Customer $customer = Auth::getProvider()->register($data, $autoActivation); - Event::fire('igniter.user.register', [$customer, $data]); + Event::dispatch('igniter.user.register', [$customer, $data]); if ($autoActivation) { Auth::login($customer); @@ -35,11 +35,11 @@ public function handle(array $data = []): Customer public function activate(string $code) { - throw_unless($customer = Customer::whereActivationCode($code)->first(), - new ApplicationException(lang('igniter.user::default.reset.alert_activation_failed'))); + throw_unless($customer = Customer::whereActivationCode($code)->first(), new ApplicationException( + lang('igniter.user::default.reset.alert_activation_failed'), + )); - throw_unless($customer->completeActivation($code), - new ApplicationException(lang('igniter.user::default.reset.alert_activation_failed'))); + throw_unless($customer->completeActivation($code), new ApplicationException('User is already active!')); Auth::login($customer); diff --git a/src/Classes/Notification.php b/src/Classes/Notification.php index ca2fafa..74e64c7 100644 --- a/src/Classes/Notification.php +++ b/src/Classes/Notification.php @@ -25,9 +25,9 @@ class Notification extends BaseNotification implements ShouldQueue protected ?string $iconColor = null; - public static function make(array $parameters = []): static + public static function make(): static { - return app(static::class, $parameters); + return app(static::class, func_get_args()); } public function broadcast(array $users = []): static diff --git a/src/Classes/PermissionManager.php b/src/Classes/PermissionManager.php index 5fcdbdc..008248d 100644 --- a/src/Classes/PermissionManager.php +++ b/src/Classes/PermissionManager.php @@ -6,7 +6,7 @@ class PermissionManager { - protected $permissions; + protected $permissions = []; /** * @var array A cache of permissions. @@ -65,9 +65,7 @@ public function listGroupedPermissions() $grouped = []; foreach ($this->listPermissions() as $permission) { - $group = isset($permission->group) - ? strtolower($permission->group) - : 'Undefined group'; + $group = strtolower(strlen($permission->group) ? $permission->group : 'Undefined group'); $permission->group ??= $group; @@ -106,34 +104,38 @@ public function checkPermission($permissions, $checkPermissions, $checkAll) protected function checkPermissionStartsWith($permission, $permissions) { - if (strlen($permission) > 1 && ends_with($permission, '*')) { - $checkPermission = substr($permission, 0, -1); - - foreach ($permissions as $groupPermission => $permitted) { - // Let's make sure the available permission starts with our permission - if ($checkPermission != $groupPermission - && starts_with($groupPermission, $checkPermission) - && $permitted == 1 - ) { - return true; - } + $checkPermission = (strlen($permission) > 1 && ends_with($permission, '*')) + ? substr($permission, 0, -1) : $permission; + + foreach ($permissions as $groupPermission => $permitted) { + $groupPermission = (strlen($groupPermission) > 1 && ends_with($groupPermission, '*')) + ? substr($groupPermission, 0, -1) : $groupPermission; + + // Let's make sure the available permission starts with our permission + if ($checkPermission != $groupPermission + && (starts_with($groupPermission, $checkPermission) || starts_with($checkPermission, $groupPermission)) + && $permitted == 1 + ) { + return true; } } } protected function checkPermissionEndsWith($permission, $permissions) { - if (strlen($permission) > 1 && starts_with($permission, '*')) { - $checkPermission = substr($permission, 1); - - foreach ($permissions as $groupPermission => $permitted) { - // Let's make sure the available permission ends with our permission - if ($checkPermission != $groupPermission - && ends_with($groupPermission, $checkPermission) - && $permitted == 1 - ) { - return true; - } + $checkPermission = (strlen($permission) > 1 && starts_with($permission, '*')) + ? substr($permission, 1) : $permission; + + foreach ($permissions as $groupPermission => $permitted) { + $groupPermission = (strlen($groupPermission) > 1 && starts_with($groupPermission, '*')) + ? substr($groupPermission, 1) : $groupPermission; + + // Let's make sure the available permission ends with our permission + if ($checkPermission != $groupPermission + && (ends_with($groupPermission, $checkPermission) || ends_with($checkPermission, $groupPermission)) + && $permitted == 1 + ) { + return true; } } } @@ -141,18 +143,7 @@ protected function checkPermissionEndsWith($permission, $permissions) protected function checkPermissionMatches($permission, $permissions) { foreach ($permissions as $groupPermission => $permitted) { - if ((strlen($groupPermission) > 1) && ends_with($groupPermission, '*')) { - $checkMergedPermission = substr($groupPermission, 0, -1); - - // Let's make sure the our permission starts with available permission - if ($checkMergedPermission != $permission - && starts_with($permission, $checkMergedPermission) - && $permitted == 1 - ) { - return true; - } - } // Match permissions explicitly. - elseif ($permission == $groupPermission && $permitted == 1) { + if ($permission == $groupPermission && $permitted == 1) { return true; } } @@ -164,10 +155,6 @@ protected function checkPermissionMatches($permission, $permissions) public function registerPermissions($owner, array $definitions) { - if (!$this->permissions) { - $this->permissions = []; - } - foreach ($definitions as $code => $definition) { if (!isset($definition['label']) && isset($definition['description'])) { $definition['label'] = $definition['description']; diff --git a/src/Classes/UserState.php b/src/Classes/UserState.php index ddd5953..ebd9a24 100644 --- a/src/Classes/UserState.php +++ b/src/Classes/UserState.php @@ -120,7 +120,7 @@ public static function getClearAfterMinutesDropdownOptions() public function updateState(string $status, string $message, int $clearAfterMinutes = 30) { - UserPreference::onUser()->set(self::USER_PREFERENCE_KEY, array_merge($this->defaultStateConfig, [ + UserPreference::onUser($this->user)->set(self::USER_PREFERENCE_KEY, array_merge($this->defaultStateConfig, [ 'status' => $status, 'updatedAt' => now(), 'awayMessage' => e($message), @@ -130,7 +130,7 @@ public function updateState(string $status, string $message, int $clearAfterMinu $this->stateConfigCache = null; } - protected function getConfig($key = null, $default = null) + public function getConfig($key = null, $default = null) { if (is_null($this->stateConfigCache)) { $this->stateConfigCache = $this->loadConfigFromPreference(); diff --git a/src/Console/Commands/ClearUserStateCommand.php b/src/Console/Commands/ClearUserStateCommand.php index 3b84cb3..c7b46b9 100644 --- a/src/Console/Commands/ClearUserStateCommand.php +++ b/src/Console/Commands/ClearUserStateCommand.php @@ -23,18 +23,15 @@ public function handle(): void ->where('value->clearAfterMinutes', '!=', 0) ->get() ->each(function($preference) { - $state = json_decode($preference->value); - if (!$state->clearAfterMinutes) { - return true; - } - - if (now()->lessThan(make_carbon($state->updatedAt)->addMinutes($state->clearAfterMinutes))) { + $clearAfterMinutes = $preference->value['clearAfterMinutes'] ?? 0; + $updatedAt = $preference->value['updatedAt'] ?? null; + if (!$clearAfterMinutes || now()->lessThan(make_carbon($updatedAt)->addMinutes($clearAfterMinutes))) { return true; } UserPreference::query() ->where('id', $preference->id) - ->update(['value' => json_encode((new static)->defaultStateConfig)]); + ->update(['value' => json_encode((new UserState)->getConfig())]); }); } } diff --git a/src/Database/Factories/UserFactory.php b/src/Database/Factories/UserFactory.php index 20b1cc2..bf41466 100644 --- a/src/Database/Factories/UserFactory.php +++ b/src/Database/Factories/UserFactory.php @@ -18,7 +18,7 @@ public function definition(): array 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'activated_at' => $this->faker->dateTime()->format(DateTimeInterface::ATOM), 'is_activated' => $this->faker->boolean(), - 'super_user' => $this->faker->boolean(), + 'super_user' => false, 'status' => $this->faker->boolean(), ]; } diff --git a/src/Database/Factories/UserRoleFactory.php b/src/Database/Factories/UserRoleFactory.php index 7939bf1..1be35b7 100644 --- a/src/Database/Factories/UserRoleFactory.php +++ b/src/Database/Factories/UserRoleFactory.php @@ -14,7 +14,10 @@ public function definition(): array 'code' => $this->faker->slug(2), 'name' => $this->faker->sentence(2), 'description' => $this->faker->paragraph(), - 'permissions' => [$this->faker->numberBetween(1, 99)], + 'permissions' => [ + 'Admin.Dashboard' => 1, + 'Admin.Users' => 1, + ], ]; } } diff --git a/src/Extension.php b/src/Extension.php index 32fe444..b46537b 100644 --- a/src/Extension.php +++ b/src/Extension.php @@ -249,7 +249,7 @@ public function registerFormWidgets(): array protected function registerEventGlobalParams() { if (class_exists(\Igniter\Automation\Classes\EventManager::class)) { - resolve(\Igniter\Automation\Classes\EventManager::class)->registerCallback(function($manager) { + resolve(\Igniter\Automation\Classes\EventManager::class)->registerCallback(function(\Igniter\Automation\Classes\EventManager $manager) { $manager->registerGlobalParams([ 'customer' => Auth::customer(), ]); @@ -257,19 +257,6 @@ protected function registerEventGlobalParams() } } - protected function registerRequestRebindHandler() - { - $this->app->rebinding('request', function($app, $request) { - $request->setUserResolver(function() use ($app) { - if (!Igniter::runningInAdmin()) { - return $app['admin.auth']->getUser(); - } - - return $app['main.auth']->user(); - }); - }); - } - protected function configureRateLimiting() { RateLimiter::for('web', function(\Illuminate\Http\Request $request) { diff --git a/src/Http/Actions/AssigneeController.php b/src/Http/Actions/AssigneeController.php index f182313..7d9c627 100644 --- a/src/Http/Actions/AssigneeController.php +++ b/src/Http/Actions/AssigneeController.php @@ -44,13 +44,11 @@ public function __construct($controller) ]); $this->controller->bindEvent('controller.beforeRemap', function() { - if (!$this->controller->getUser()) { - return; + if ($this->controller->getUser()) { + $this->assigneeBindToolbarEvents(); + $this->assigneeBindListsEvents(); + $this->assigneeBindFormEvents(); } - - $this->assigneeBindToolbarEvents(); - $this->assigneeBindListsEvents(); - $this->assigneeBindFormEvents(); }); } @@ -89,19 +87,15 @@ protected function assigneeBindListsEvents() { if ($this->controller->isClassExtendedWith(\Igniter\Admin\Http\Actions\ListController::class)) { Event::listen('admin.list.extendQuery', function($listWidget, $query) { - if (!(bool)$this->getConfig('applyScopeOnListQuery', true)) { - return; + if ($this->getConfig('applyScopeOnListQuery', true)) { + $this->assigneeApplyScope($query); } - - $this->assigneeApplyScope($query); }); Event::listen('admin.filter.extendScopesBefore', function($widget) { - if (!$this->controller->getUser()->hasRestrictedAssignableScope()) { - return; + if ($this->controller->getUser()->hasRestrictedAssignableScope()) { + unset($widget->scopes['assignee']); } - - unset($widget->scopes['assignee']); }); } } @@ -110,38 +104,25 @@ protected function assigneeBindFormEvents() { if ($this->controller->isClassExtendedWith(\Igniter\Admin\Http\Actions\FormController::class)) { $this->controller->bindEvent('admin.controller.extendFormQuery', function($query) { - if (!(bool)$this->getConfig('applyScopeOnFormQuery', true)) { - return; + if ($this->getConfig('applyScopeOnFormQuery', true)) { + $this->assigneeApplyScope($query); } - - $this->assigneeApplyScope($query); }); Event::listen('admin.form.extendFields', function(Form $widget) { - if (!is_a($widget->getController(), get_class($this->controller))) { - return; - } - - if (!in_array(Assignable::class, class_uses_recursive(get_class($widget->model)))) { - return; - } - $assignable = $widget->model; - if (!$assignable->hasAssignToGroup() || $assignable->hasAssignTo()) { - return; - } - - // Let the allocator handle assignment when auto assign is enabled - if ($assignable->assignee_group->autoAssignEnabled()) { - return; - } - $user = $this->controller->getUser(); - if ($assignable->cannotAssignToStaff($user)) { - return; - } - $assignable->assignTo($user); + if ( + is_a($widget->getController(), get_class($this->controller)) + && in_array(Assignable::class, class_uses_recursive(get_class($widget->model))) + && $assignable->hasAssignToGroup() + && !$assignable->hasAssignTo() + && !$assignable->assignee_group->autoAssignEnabled() + && !$assignable->cannotAssignToStaff($user) + ) { + $assignable->assignTo($user); + } }); } } diff --git a/src/Http/Controllers/Customers.php b/src/Http/Controllers/Customers.php index 9d77280..e5b2da3 100644 --- a/src/Http/Controllers/Customers.php +++ b/src/Http/Controllers/Customers.php @@ -71,7 +71,7 @@ public function __construct() public function index_onDelete() { throw_unless($this->authorize('Admin.DeleteCustomers'), - new FlashException(lang('igniter::admin.alert_user_restricted')) + new FlashException(lang('igniter::admin.alert_user_restricted')), ); return $this->asExtension(\Igniter\Admin\Http\Actions\ListController::class)->index_onDelete(); @@ -80,7 +80,7 @@ public function index_onDelete() public function edit_onDelete($context, $recordId) { throw_unless($this->authorize('Admin.DeleteCustomers'), - new FlashException(lang('igniter::admin.alert_user_restricted')) + new FlashException(lang('igniter::admin.alert_user_restricted')), ); return $this->asExtension(\Igniter\Admin\Http\Actions\FormController::class)->edit_onDelete($context, $recordId); @@ -89,7 +89,7 @@ public function edit_onDelete($context, $recordId) public function onImpersonate($context, $recordId = null) { throw_unless($this->authorize('Admin.ImpersonateCustomers'), - new FlashException(lang('igniter.user::default.customers.alert_login_restricted')) + new FlashException(lang('igniter.user::default.customers.alert_login_restricted')), ); $id = post('recordId', $recordId); @@ -104,7 +104,7 @@ public function edit_onActivate($context, $recordId = null) { if ($customer = $this->formFindModelObject((int)$recordId)) { $customer->completeActivation($customer->getActivationCode()); - flash()->success(sprintf(lang('igniter.user::default.customers.alert_activation_success'), $customer->full_name)); + flash()->success(lang('igniter.user::default.customers.alert_activation_success')); } return $this->redirectBack(); @@ -119,15 +119,4 @@ public function formExtendModel($model) ]); } } - - public function formAfterSave($model) - { - if (!$model->group || $model->group->requiresApproval()) { - return; - } - - if ($this->status && !$this->is_activated) { - $model->completeActivation($model->getActivationCode()); - } - } } diff --git a/src/Http/Controllers/Login.php b/src/Http/Controllers/Login.php index e338330..2d5a5be 100644 --- a/src/Http/Controllers/Login.php +++ b/src/Http/Controllers/Login.php @@ -87,10 +87,7 @@ public function onRequestResetPassword() ]); if ($user = User::whereEmail($data['email'])->first()) { - if (!$user->resetPassword()) { - throw ValidationException::withMessages(['email' => lang('igniter::admin.login.alert_failed_reset')]); - } - + $user->resetPassword(); $user->mailSendResetPasswordRequest([ 'reset_link' => admin_url('login/reset?code='.$user->reset_code), ]); diff --git a/src/Http/Controllers/Users.php b/src/Http/Controllers/Users.php index 98d1991..8a45be7 100644 --- a/src/Http/Controllers/Users.php +++ b/src/Http/Controllers/Users.php @@ -108,7 +108,7 @@ public function account_onSave() public function index_onDelete() { throw_unless($this->authorize('Admin.DeleteStaffs'), - new FlashException(lang('igniter::admin.alert_user_restricted')) + new FlashException(lang('igniter::admin.alert_user_restricted')), ); return $this->asExtension(\Igniter\Admin\Http\Actions\ListController::class)->index_onDelete(); @@ -117,7 +117,7 @@ public function index_onDelete() public function edit_onDelete($context, $recordId) { throw_unless($this->authorize('Admin.DeleteStaffs'), - new FlashException(lang('igniter::admin.alert_user_restricted')) + new FlashException(lang('igniter::admin.alert_user_restricted')), ); return $this->asExtension(\Igniter\Admin\Http\Actions\FormController::class)->edit_onDelete($context, $recordId); @@ -126,14 +126,14 @@ public function edit_onDelete($context, $recordId) public function onImpersonate($context, $recordId = null) { throw_unless($this->authorize('Admin.Impersonate'), - new FlashException(lang('igniter.user::default.staff.alert_login_restricted')) + new FlashException(lang('igniter.user::default.staff.alert_login_restricted')), ); $id = post('recordId', $recordId); if ($user = $this->formFindModelObject((int)$id)) { AdminAuth::stopImpersonate(); AdminAuth::impersonate($user); - flash()->success(sprintf(lang('igniter.user::default.customers.alert_impersonate_success'), $user->name)); + flash()->success(sprintf(lang('igniter.user::default.staff.alert_impersonate_success'), $user->name)); } return $this->redirect('dashboard'); @@ -161,11 +161,4 @@ public function formExtendFields($form) $form->removeField('super_user'); } } - - public function formAfterSave($model) - { - if ($this->status && !$this->is_activated) { - $model->completeActivation($model->getActivationCode()); - } - } } diff --git a/src/Http/Middleware/InjectImpersonateBanner.php b/src/Http/Middleware/InjectImpersonateBanner.php index 898c949..dea3bd2 100644 --- a/src/Http/Middleware/InjectImpersonateBanner.php +++ b/src/Http/Middleware/InjectImpersonateBanner.php @@ -4,6 +4,7 @@ use Igniter\Flame\Igniter; use Igniter\User\Facades\Auth; +use Illuminate\Support\Facades\View; use Symfony\Component\HttpFoundation\Response; class InjectImpersonateBanner @@ -28,7 +29,7 @@ public function handle($request, \Closure $next): Response protected function injectBanner(Response $response): void { $content = $response->getContent(); - $banner = view('igniter.user::_partials.impersonate_banner')->render(); + $banner = View::make('igniter.user::_partials.impersonate_banner')->render(); $pos = strripos($content, ''); if ($pos !== false) { $content = substr($content, 0, $pos).$banner.substr($content, $pos); diff --git a/src/Jobs/AllocateAssignable.php b/src/Jobs/AllocateAssignable.php index 4b7ad10..779648e 100644 --- a/src/Jobs/AllocateAssignable.php +++ b/src/Jobs/AllocateAssignable.php @@ -76,10 +76,6 @@ public function handle() protected function waitInSecondsAfterAttempt(int $attempt) { - if ($attempt > 3) { - return 1000; - } - - return 10 ** $attempt; + return $attempt >= 3 ? 1000 : 10 ** $attempt; } } diff --git a/src/MainMenuWidgets/UserPanel.php b/src/MainMenuWidgets/UserPanel.php index a7a1380..0735448 100644 --- a/src/MainMenuWidgets/UserPanel.php +++ b/src/MainMenuWidgets/UserPanel.php @@ -26,7 +26,7 @@ public function initialize() ]); $this->user = $this->getController()->getUser(); - $this->userState = UserState::forUser(); + $this->userState = UserState::forUser($this->user); } public function render() @@ -71,7 +71,7 @@ public function onSetStatus() ]); throw_if($validated['status'] < 1 && !strlen($validated['message']), - new FlashException(lang('igniter::admin.side_menu.alert_invalid_status')) + new FlashException(lang('igniter::admin.side_menu.alert_invalid_status')), ); $this->userState->updateState($validated['status'], $validated['message'] ?? '', $validated['clear_after']); @@ -113,4 +113,4 @@ protected function listMenuLinks() }) ->sortBy('priority'); } -} \ No newline at end of file +} diff --git a/src/Models/Concerns/Assignable.php b/src/Models/Concerns/Assignable.php index 46e6d35..e21c74e 100644 --- a/src/Models/Concerns/Assignable.php +++ b/src/Models/Concerns/Assignable.php @@ -83,10 +83,10 @@ public function updateAssignTo(?UserGroup $group = null, ?User $assignee = null, return $log; } - public function cannotAssignToStaff($staff) + public function cannotAssignToStaff($user) { return $this->assignable_logs() - ->where('user_id', $staff->getKey()) + ->where('user_id', $user->getKey()) ->where('assignee_group_id', $this->assignee_group_id) ->exists(); diff --git a/src/Models/Customer.php b/src/Models/Customer.php index ef4d297..cfa1274 100644 --- a/src/Models/Customer.php +++ b/src/Models/Customer.php @@ -85,7 +85,7 @@ public static function getDropdownOptions() public function getFullNameAttribute($value) { - return $this->first_name.' '.$this->last_name; + return $this->getCustomerName(); } public function getEmailAttribute($value) @@ -108,7 +108,7 @@ public function beforeLogin() } throw new SystemException(sprintf( - lang('igniter.user::default.customers.alert_customer_not_active'), $this->email + lang('igniter.user::default.customers.alert_customer_not_active'), $this->email, )); } @@ -169,7 +169,7 @@ public function saveAddresses($addresses) $customerAddress = $this->addresses()->updateOrCreate( array_only($address, ['address_id']), - array_except($address, ['address_id', 'customer_id']) + array_except($address, ['address_id', 'customer_id']), ); $idsToKeep[] = $customerAddress->getKey(); @@ -181,7 +181,7 @@ public function saveAddresses($addresses) public function saveDefaultAddress(string|int $addressId) { throw_unless($this?->addresses()->find($addressId), - new ApplicationException('Address not found or does not belong to the customer') + new ApplicationException('Address not found or does not belong to the customer'), ); $this->address_id = $addressId; @@ -193,7 +193,7 @@ public function saveDefaultAddress(string|int $addressId) public function deleteCustomerAddress(string|int $addressId) { throw_unless($address = $this?->addresses()->find($addressId), - new ApplicationException('Address not found or does not belong to the customer') + new ApplicationException('Address not found or does not belong to the customer'), ); $address->delete(); @@ -223,7 +223,7 @@ public function saveCustomerGuestOrder() Address::whereIn('address_id', Order::where('email', $this->email) ->whereNotNull('address_id') - ->pluck('address_id')->all() + ->pluck('address_id')->all(), )->update($update); return true; @@ -277,7 +277,7 @@ public function mailSendEmailVerification(array $data) 'account_activation_link' => null, ], $data); - return $this->customer->mailSend('igniter.user::mail.activation', 'customer', $data); + return $this->mailSend('igniter.user::mail.activation', 'customer', $data); } public function mailGetRecipients($type) @@ -310,10 +310,6 @@ public function register(array $attributes, $activate = false) $model->fill($attributes); $model->save(); - if ($activate && !$model->is_activated) { - $model->completeActivation($model->getActivationCode()); - } - // Prevents subsequent saves to this model object $model->password = null; diff --git a/src/Models/Observers/CustomerObserver.php b/src/Models/Observers/CustomerObserver.php index 2046606..133189c 100644 --- a/src/Models/Observers/CustomerObserver.php +++ b/src/Models/Observers/CustomerObserver.php @@ -15,12 +15,10 @@ public function saved(Customer $customer) { $customer->restorePurgedValues(); - if (!$customer->exists) { - return; - } - - if ($customer->status && is_null($customer->is_activated)) { - $customer->completeActivation($customer->getActivationCode()); + if ($customer->group && !$customer->group->requiresApproval()) { + if ($customer->status && is_null($customer->is_activated)) { + $customer->completeActivation($customer->getActivationCode()); + } } if (array_key_exists('addresses', $customer->getAttributes())) { diff --git a/src/Models/Observers/UserObserver.php b/src/Models/Observers/UserObserver.php index a7acaf4..ab17545 100644 --- a/src/Models/Observers/UserObserver.php +++ b/src/Models/Observers/UserObserver.php @@ -11,4 +11,13 @@ public function deleting(User $user) $user->groups()->detach(); $user->locations()->detach(); } + + public function saved(User $user) + { + $user->restorePurgedValues(); + + if ($user->status && is_null($user->is_activated)) { + $user->completeActivation($user->getActivationCode()); + } + } } diff --git a/src/Models/User.php b/src/Models/User.php index dac2987..f6f6875 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -123,7 +123,7 @@ public function scopeWhereIsSuperUser($query) public function afterLogin() { app('translator.localization')->setSessionLocale( - optional($this->language)->code ?? app()->getLocale() + optional($this->language)->code ?? app()->getLocale(), ); $this->query() @@ -310,33 +310,32 @@ public function addGroups($groups = []) public function register(array $attributes, $activate = false) { - $user = new static; - $user->name = array_get($attributes, 'name'); - $user->email = array_get($attributes, 'email'); - $user->username = array_get($attributes, 'username'); - $user->password = array_get($attributes, 'password'); - $user->language_id = array_get($attributes, 'language_id'); - $user->user_role_id = array_get($attributes, 'user_role_id'); - $user->super_user = array_get($attributes, 'super_user', false); - $user->status = array_get($attributes, 'status', true); - $user->save(); + $this->name = array_get($attributes, 'name'); + $this->email = array_get($attributes, 'email'); + $this->username = array_get($attributes, 'username'); + $this->password = array_get($attributes, 'password'); + $this->language_id = array_get($attributes, 'language_id'); + $this->user_role_id = array_get($attributes, 'user_role_id'); + $this->super_user = array_get($attributes, 'super_user', false); + $this->status = array_get($attributes, 'status', true); + $this->save(); if ($activate) { - $user->completeActivation($user->getActivationCode()); + $this->completeActivation($this->getActivationCode()); } // Prevents subsequent saves to this model object - $user->password = null; + $this->password = null; if (array_key_exists('groups', $attributes)) { - $user->groups()->attach($attributes['groups']); + $this->groups()->attach($attributes['groups']); } if (array_key_exists('locations', $attributes)) { - $user->locations()->attach($attributes['locations']); + $this->locations()->attach($attributes['locations']); } - return $user->reload(); + return $this->reload(); } public function receivesBroadcastNotificationsOn() diff --git a/src/Models/UserRole.php b/src/Models/UserRole.php index 76160f6..5327d95 100644 --- a/src/Models/UserRole.php +++ b/src/Models/UserRole.php @@ -56,9 +56,9 @@ public function getStaffCountAttribute($value) public function setPermissionsAttribute($permissions) { foreach ($permissions ?? [] as $permission => $value) { - if (!in_array($value = (int)$value, [-1, 0, 1])) { + if (!in_array((int)$value, [-1, 0, 1])) { throw new InvalidArgumentException(sprintf( - 'Invalid value "%s" for permission "%s" given.', $value, $permission + 'Invalid value "%s" for permission "%s" given.', $value, $permission, )); } diff --git a/src/Notifications/AssigneeUpdatedNotification.php b/src/Notifications/AssigneeUpdatedNotification.php index c549fc2..a6a832f 100644 --- a/src/Notifications/AssigneeUpdatedNotification.php +++ b/src/Notifications/AssigneeUpdatedNotification.php @@ -4,6 +4,7 @@ use Igniter\Cart\Models\Order; use Igniter\User\Classes\Notification; +use Igniter\User\Facades\AdminAuth; class AssigneeUpdatedNotification extends Notification { @@ -12,7 +13,7 @@ public function getRecipients(): array $recipients = []; if (!$this->subject->assignee && $this->subject->assignee_group) { foreach ($this->subject->assignable->listGroupAssignees() as $assignee) { - if (auth()->user() && $assignee->getKey() === auth()->user()->getKey()) { + if (AdminAuth::user() && $assignee->getKey() === AdminAuth::user()->getKey()) { continue; } @@ -48,9 +49,7 @@ public function getMessage(): string ? lang('igniter.cart::default.orders.notify_assigned') : lang('igniter.reservation::default.notify_assigned'); - $causerName = $this->subject->user - ? $this->subject->user->full_name - : lang('igniter::admin.text_system'); + $causerName = $this->subject->user ? $this->subject->user->full_name : lang('igniter::admin.text_system'); $assigneeName = ''; if ($this->subject->assignee) { diff --git a/src/Subscribers/ConsoleSubscriber.php b/src/Subscribers/ConsoleSubscriber.php index 77cba85..2c57d5b 100644 --- a/src/Subscribers/ConsoleSubscriber.php +++ b/src/Subscribers/ConsoleSubscriber.php @@ -3,7 +3,7 @@ namespace Igniter\User\Subscribers; use Illuminate\Console\Scheduling\Schedule; -use Illuminate\Events\Dispatcher; +use Illuminate\Contracts\Events\Dispatcher; class ConsoleSubscriber { @@ -37,4 +37,4 @@ protected function clearUserExpiredCustomAwayStatus(Schedule $schedule): void ->runInBackground() ->everyMinute(); } -} \ No newline at end of file +} diff --git a/tests/Actions/LoginCustomerTest.php b/tests/Actions/LoginCustomerTest.php new file mode 100644 index 0000000..d062543 --- /dev/null +++ b/tests/Actions/LoginCustomerTest.php @@ -0,0 +1,34 @@ + 'user@example.com', 'password' => 'password']; + Auth::shouldReceive('attempt')->andReturn(true); + Session::shouldReceive('regenerate')->once(); + + $loginUser = new LoginCustomer($credentials); + $loginUser->handle(); + + Event::assertDispatched('igniter.user.beforeAuthenticate', function($eventName, $eventPayload) use ($loginUser, $credentials) { + return $eventPayload[0] === $loginUser && $eventPayload[1] === $credentials; + }); + + Event::assertDispatched('igniter.user.login', function($eventName, $eventPayload) use ($loginUser) { + return $eventPayload[0] === $loginUser; + }, true); +}); + +it('throws FlashException when authentication fails', function() { + $credentials = ['email' => 'user@example.com', 'password' => 'wrongpassword']; + Auth::shouldReceive('attempt')->andReturn(false); + + expect(fn() => (new LoginCustomer($credentials))->handle())->toThrow(FlashException::class); +}); diff --git a/tests/Actions/LogoutCustomerTest.php b/tests/Actions/LogoutCustomerTest.php new file mode 100644 index 0000000..8639be6 --- /dev/null +++ b/tests/Actions/LogoutCustomerTest.php @@ -0,0 +1,61 @@ +andReturn($customer); + Auth::shouldReceive('isImpersonator')->andReturn(false); + Auth::shouldReceive('logout')->once(); + Session::shouldReceive('invalidate')->once(); + Session::shouldReceive('regenerateToken')->once(); + + (new LogoutCustomer)->handle(); + + expect(flash()->messages()->first()) + ->level->toBe('success') + ->message->toBe(lang('igniter.user::default.alert_logout_success')); + + Event::assertDispatched('igniter.user.logout', function($eventName, $eventPayload) use ($customer) { + return $eventPayload[0] === $customer; + }); +}); + +it('stops impersonation when customer is impersonating', function() { + Event::fake(); + Auth::shouldReceive('getUser')->andReturn(null); + Auth::shouldReceive('isImpersonator')->andReturn(true); + Auth::shouldReceive('stopImpersonate')->once(); + Session::shouldReceive('invalidate')->never(); + Session::shouldReceive('regenerateToken')->never(); + + (new LogoutCustomer)->handle(); + + expect(flash()->messages()->first()) + ->level->toBe('success') + ->message->toBe(lang('igniter.user::default.alert_logout_success')); + + Event::assertNotDispatched('igniter.user.logout'); +}); + +it('does not dispatch logout event when customer is null', function() { + Event::fake(); + Auth::shouldReceive('getUser')->andReturn(null); + Auth::shouldReceive('isImpersonator')->andReturn(false); + Auth::shouldReceive('logout')->once(); + Session::shouldReceive('invalidate')->once(); + Session::shouldReceive('regenerateToken')->once(); + + (new LogoutCustomer)->handle(); + + expect(flash()->messages()->first()) + ->level->toBe('success') + ->message->toBe(lang('igniter.user::default.alert_logout_success')); + + Event::assertNotDispatched('igniter.user.logout'); +}); diff --git a/tests/Actions/RegisterCustomerTest.php b/tests/Actions/RegisterCustomerTest.php new file mode 100644 index 0000000..3b48b46 --- /dev/null +++ b/tests/Actions/RegisterCustomerTest.php @@ -0,0 +1,60 @@ + 'user@example.com', 'password' => 'password', 'customer_group_id' => 1]; + Auth::shouldReceive('getProvider->register')->with($data, true)->andReturn(Mockery::mock(Customer::class)); + Auth::shouldReceive('login')->once(); + + $result = (new RegisterCustomer)->handle($data); + + expect($result)->toBeInstanceOf(Customer::class); + + Event::assertDispatched('igniter.user.beforeRegister', function($eventName, $eventPayload) use ($data) { + return $eventPayload[0] === $data; + }); + Event::assertDispatched('igniter.user.register', function($eventName, $eventPayload) use ($result, $data) { + return $eventPayload[0] === $result && $eventPayload[1] === $data; + }); +}); + +it('throws exception when activation code is invalid', function() { + expect(fn() => (new RegisterCustomer)->activate('invalid_code'))->toThrow( + ApplicationException::class, lang('igniter.user::default.reset.alert_activation_failed'), + ); +}); + +it('throws exception when activation fails', function() { + Customer::factory()->create([ + 'status' => false, + 'is_activated' => true, + 'activation_code' => 'valid_code', + ]); + + expect(fn() => (new RegisterCustomer)->activate('valid_code'))->toThrow( + SystemException::class, 'User is already active!', + ); +}); + +it('activates customer and logs in successfully', function() { + $customer = Customer::factory()->create([ + 'status' => false, + 'is_activated' => false, + 'activation_code' => 'valid_code', + ]); + Auth::shouldReceive('login')->once(); + + $result = (new RegisterCustomer)->activate('valid_code'); + + expect($result->getKey())->toBe($customer->getKey()); +}); diff --git a/tests/Auth/AuthServiceProviderTest.php b/tests/Auth/AuthServiceProviderTest.php new file mode 100644 index 0000000..c91a6f5 --- /dev/null +++ b/tests/Auth/AuthServiceProviderTest.php @@ -0,0 +1,87 @@ +app); + $serviceProvider->register(); + + $config = config('igniter-auth'); + + expect($config)->not->toBeNull() + ->toHaveKey('guards') + ->toHaveKey('mergeGuards') + ->toHaveKey('mergeProviders') + ->and($config['guards']['admin'])->toBe('igniter-admin') + ->and($config['guards']['web'])->toBe('igniter-customer') + ->and($config['mergeGuards']['igniter-admin'])->toBe([ + 'driver' => 'igniter-admin', + 'provider' => 'igniter-admin', + ]) + ->and($config['mergeGuards']['igniter-customer'])->toBe([ + 'driver' => 'igniter-customer', + 'provider' => 'igniter', + ]) + ->and($config['mergeProviders']['igniter-admin'])->toBe([ + 'driver' => 'igniter', + 'model' => \Igniter\User\Models\User::class, + ]) + ->and($config['mergeProviders']['igniter'])->toBe([ + 'driver' => 'igniter', + 'model' => Customer::class, + ]); +}); + +it('publishes configuration when running in console', function() { + $app = Mockery::mock(Application::class)->makePartial(); + $app->shouldReceive('booted')->once(); + $app->shouldReceive('runningInConsole')->andReturn(true)->once(); + $serviceProvider = new AuthServiceProvider($app); + $serviceProvider->boot(); + + expect(true)->toBeTrue(); +}); + +it('configures auth guards correctly', function() { + $serviceProvider = new AuthServiceProvider($this->app); + $serviceProvider->boot(); + + $guards = config('auth.guards'); + expect($guards)->toHaveKey('igniter-admin') + ->and($guards)->toHaveKey('igniter-customer'); +}); + +it('configures auth provider correctly', function() { + $serviceProvider = new AuthServiceProvider($this->app); + $serviceProvider->boot(); + + $providers = config('auth.providers'); + expect($providers)->toHaveKey('igniter'); +}); + +it('configures gate callback correctly', function() { + Gate::shouldReceive('after')->with(Mockery::on(function($callback) { + $adminUser = Mockery::mock(\Igniter\User\Models\User::class); + $adminUser->shouldReceive('hasAnyPermission')->with('ability')->andReturn(true); + + expect($callback($adminUser, 'ability'))->toBeTrue(); + + return true; + })); + + $serviceProvider = new AuthServiceProvider($this->app); + $serviceProvider->boot(); +}); + +//it('creates guard with correct configuration', function () { +// $serviceProvider = new AuthServiceProvider($this->app); +// $guard = $serviceProvider->createGuard(UserGuard::class, 'igniter - admin', [], Auth::guard()); +// +// expect($guard)->toBeInstanceOf(UserGuard::class); +//}); diff --git a/tests/Auth/CustomerGuardTest.php b/tests/Auth/CustomerGuardTest.php new file mode 100644 index 0000000..0e7315c --- /dev/null +++ b/tests/Auth/CustomerGuardTest.php @@ -0,0 +1,114 @@ +makePartial(); + $guard->shouldReceive('user')->andReturn($user); + + $result = $guard->customer(); + + expect($result)->toBe($user); +}); + +it('checks if customer is logged in', function() { + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $guard->shouldReceive('check')->andReturn(true); + + $result = $guard->isLogged(); + + expect($result)->toBeTrue(); +}); + +it('returns customer id', function() { + $user = Mockery::mock(Customer::class)->makePartial(); + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $user->customer_id = 1; + $guard->setUser($user); + + $result = $guard->getId(); + + expect($result)->toBe(1); +}); + +it('returns customer full name', function() { + $user = Mockery::mock(Customer::class)->makePartial(); + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $user->shouldReceive('extendableGet')->with('full_name')->andReturn('John Doe'); + $guard->setUser($user); + + $result = $guard->getFullName(); + + expect($result)->toBe('John Doe'); +}); + +it('returns customer first name', function() { + $user = Mockery::mock(Customer::class)->makePartial(); + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $user->first_name = 'John'; + $guard->setUser($user); + + $result = $guard->getFirstName(); + + expect($result)->toBe('John'); +}); + +it('returns customer last name', function() { + $user = Mockery::mock(Customer::class)->makePartial(); + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $user->last_name = 'Doe'; + $guard->setUser($user); + + $result = $guard->getLastName(); + + expect($result)->toBe('Doe'); +}); + +it('returns customer email in lowercase', function() { + $user = Mockery::mock(Customer::class)->makePartial(); + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $user->email = 'John.Doe@Example.com'; + $guard->setUser($user); + + $result = $guard->getEmail(); + + expect($result)->toBe('john.doe@example.com'); +}); + +it('returns customer telephone', function() { + $user = Mockery::mock(Customer::class)->makePartial(); + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $user->telephone = '1234567890'; + $guard->setUser($user); + + $result = $guard->getTelephone(); + + expect($result)->toBe('1234567890'); +}); + +it('returns customer address id', function() { + $user = Mockery::mock(Customer::class)->makePartial(); + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $user->address_id = 1; + $guard->setUser($user); + + $result = $guard->getAddressId(); + + expect($result)->toBe(1); +}); + +it('returns customer group id', function() { + $user = Mockery::mock(Customer::class)->makePartial(); + $guard = Mockery::mock(CustomerGuard::class)->makePartial(); + $user->customer_group_id = 1; + $guard->setUser($user); + + $result = $guard->getGroupId(); + + expect($result)->toBe(1); +}); diff --git a/tests/Auth/GuardHelpersTest.php b/tests/Auth/GuardHelpersTest.php new file mode 100644 index 0000000..177a7b5 --- /dev/null +++ b/tests/Auth/GuardHelpersTest.php @@ -0,0 +1,144 @@ +makePartial(); + $guard = Mockery::mock(UserGuard::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $user->shouldReceive('beforeLogin')->once(); + $user->shouldReceive('afterLogin')->once(); + $guard->shouldReceive('login')->passthru(); + $guard->shouldReceive('updateSession')->once(); + $guard->shouldReceive('fireLoginEvent')->once(); + $guard->shouldReceive('setUser')->once(); + + $guard->login($user); +}); + +it('retrieves user by id', function() { + $user = Mockery::mock(User::class)->makePartial(); + $provider = Mockery::mock(UserProvider::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $provider->shouldReceive('retrieveById')->with(1)->andReturn($user); + $guard->shouldReceive('getProvider')->andReturn($provider); + + $result = $guard->getById(1); + + expect($result)->toBe($user); +}); + +it('retrieves user by token', function() { + $user = Mockery::mock(User::class)->makePartial(); + $provider = Mockery::mock(UserProvider::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $provider->shouldReceive('retrieveByToken')->with(1, 'token')->andReturn($user); + $guard->shouldReceive('getProvider')->andReturn($provider); + + $result = $guard->getByToken(1, 'token'); + + expect($result)->toBe($user); +}); + +it('retrieves user by credentials', function() { + $user = Mockery::mock(User::class)->makePartial(); + $provider = Mockery::mock(UserProvider::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $provider->shouldReceive('retrieveByCredentials')->with(['email' => 'test@example.com'])->andReturn($user); + $guard->shouldReceive('getProvider')->andReturn($provider); + + $result = $guard->getByCredentials(['email' => 'test@example.com']); + + expect($result)->toBe($user); +}); + +it('validates user credentials', function() { + $user = Mockery::mock(User::class)->makePartial(); + $provider = Mockery::mock(UserProvider::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $provider->shouldReceive('validateCredentials')->with($user, ['password' => 'secret'])->andReturn(true); + $guard->shouldReceive('getProvider')->andReturn($provider); + + $result = $guard->validateCredentials($user, ['password' => 'secret']); + + expect($result)->toBeTrue(); +}); + +it('impersonates user and sets session properties', function() { + $user = Mockery::mock(User::class)->makePartial(); + $oldUser = Mockery::mock(User::class)->makePartial(); + $session = Mockery::mock(Store::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + setObjectProtectedProperty($guard, 'session', $session); + $session->shouldReceive('get')->with('login')->andReturn(1); + $session->shouldReceive('has')->with('login_impersonate')->andReturnFalse()->once(); + $session->shouldReceive('put')->with('login_impersonate', 1)->once(); + $guard->shouldReceive('getName')->andReturn('login'); + $guard->shouldReceive('getById')->with(1)->andReturn($oldUser); + $guard->shouldReceive('login')->with($user); + $user->shouldReceive('fireEvent')->with('model.auth.beforeImpersonate', [$oldUser])->once(); + + $guard->impersonate($user); +}); + +it('stops impersonation and restores original user', function() { + $user = Mockery::mock(User::class)->makePartial(); + $oldUser = Mockery::mock(User::class)->makePartial(); + $session = Mockery::mock(Store::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->shouldReceive('getById')->with(1)->andReturn($oldUser); + $guard->shouldReceive('getById')->with(2)->andReturn($user); + $session->shouldReceive('get')->with('login')->andReturn(2); + $session->shouldReceive('pull')->with('login_impersonate')->andReturn(1); + $session->shouldReceive('remove')->with('login'); + setObjectProtectedProperty($guard, 'session', $session); + $guard->shouldReceive('getName')->andReturn('login'); + $guard->shouldReceive('login')->with($oldUser); + $user->shouldReceive('fireEvent')->with('model.auth.afterImpersonate', [$oldUser])->once(); + + $guard->stopImpersonate(); +}); + +it('checks if user is impersonator', function() { + $session = Mockery::mock(Store::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->shouldReceive('getName')->andReturn('login'); + $session->shouldReceive('has')->with('login_impersonate')->andReturn(true); + setObjectProtectedProperty($guard, 'session', $session); + + $result = $guard->isImpersonator(); + + expect($result)->toBeTrue(); +}); + +it('retrieves impersonator user', function() { + $user = Mockery::mock(User::class)->makePartial(); + $session = Mockery::mock(Store::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->shouldReceive('getName')->andReturn('login'); + $session->shouldReceive('get')->with('login_impersonate')->andReturn(1); + setObjectProtectedProperty($guard, 'session', $session); + $guard->shouldReceive('getById')->with(1)->andReturn($user); + + $result = $guard->getImpersonator(); + + expect($result)->toBe($user); +}); + +it('retrieves impersonator user returns false', function() { + $user = Mockery::mock(User::class)->makePartial(); + $session = Mockery::mock(Store::class); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->shouldReceive('getName')->andReturn('login'); + $session->shouldReceive('get')->with('login_impersonate')->andReturn(null); + setObjectProtectedProperty($guard, 'session', $session); + + $result = $guard->getImpersonator(); + + expect($result)->toBeFalse(); +}); diff --git a/tests/Auth/Models/UserTest.php b/tests/Auth/Models/UserTest.php new file mode 100644 index 0000000..c2cf278 --- /dev/null +++ b/tests/Auth/Models/UserTest.php @@ -0,0 +1,217 @@ +beforeLogin())->toBeNull(); +}); + +it('calls afterLogin without errors', function() { + $user = new User; + expect($user->afterLogin())->toBeNull(); +}); + +it('extends user query without errors', function() { + $user = new User; + $query = Mockery::mock(Builder::class); + expect($user->extendUserQuery($query))->toBeNull(); +}); + +it('returns correct remember token name', function() { + $user = new User; + expect($user->getRememberTokenName())->toBe('remember_token'); +}); + +it('updates remember token correctly', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('setRememberToken')->with('new_token')->once(); + $user->shouldReceive('save')->once(); + + $user->updateRememberToken('new_token'); +}); + +it('returns false when checking remember token with null token', function() { + $user = new User; + $user->remember_token = null; + + expect($user->checkRememberToken(null))->toBeFalse(); +}); + +it('returns false when checking remember token with incorrect token', function() { + $user = new User; + $user->remember_token = 'correct_token'; + + expect($user->checkRememberToken('incorrect_token'))->toBeFalse(); +}); + +it('returns true when checking remember token with correct token', function() { + $user = new User; + $user->remember_token = 'correct_token'; + + expect($user->checkRememberToken('correct_token'))->toBeTrue(); +}); + +it('updates last seen correctly', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('newQuery->whereKey->update')->with(['last_seen' => '2023-10-10 00:00:00'])->once(); + + $user->updateLastSeen('2023-10-10 00:00:00'); +}); + +it('generates a unique reset code and sets reset time', function() { + $user = Mockery::mock(User::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $user->shouldReceive('generateResetCode')->andReturn('new_reset_code'); + $user->shouldReceive('save')->once(); + + $resetCode = $user->resetPassword(); + + expect($resetCode)->toBe('new_reset_code') + ->and($user->reset_code)->not->toBeNull() + ->and($user->reset_time)->not->toBeNull(); +}); + +it('handles reset code collision by generating a new one', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('newQuery->where->count')->andReturn(1, 1, 0); + $user->shouldReceive('save')->once(); + + $resetCode = $user->resetPassword(); + + expect($resetCode)->not->toBeNull() + ->and($user->reset_code)->toBe($resetCode); +}); + +it('clears reset password code and time', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->reset_code = 'some_code'; + $user->reset_time = Carbon::now(); + $user->shouldReceive('save')->once(); + + $user->clearResetPasswordCode(); + + expect($user->reset_code)->toBeNull() + ->and($user->reset_time)->toBeNull(); +}); + +it('completes reset password successfully with valid code', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('checkResetPasswordCode')->with('valid_code')->andReturn(true); + $user->shouldReceive('save')->andReturnTrue()->once(); + + $result = $user->completeResetPassword('valid_code', 'new_password'); + + expect($result)->toBeTrue() + ->and(Hash::check('new_password', $user->password))->toBeTrue() + ->and($user->reset_code)->toBeNull() + ->and($user->reset_time)->toBeNull(); +}); + +it('fails to complete reset password with invalid code', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('checkResetPasswordCode')->with('invalid_code')->andReturn(false); + + $result = $user->completeResetPassword('invalid_code', 'new_password'); + + expect($result)->toBeFalse(); +}); + +it('returns true when reset code is valid and not expired', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->reset_code = 'valid_code'; + $user->reset_time = Carbon::now()->subMinutes(10); + + $result = $user->checkResetPasswordCode('valid_code'); + + expect($result)->toBeTrue(); +}); + +it('returns false when reset code is invalid', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->reset_code = 'valid_code'; + + $result = $user->checkResetPasswordCode('invalid_code'); + + expect($result)->toBeFalse(); +}); + +it('returns false when reset code is expired', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->reset_code = 'valid_code'; + $user->reset_time = Carbon::now()->subMinutes(1500); + $user->shouldReceive('clearResetPasswordCode')->once(); + + $result = $user->checkResetPasswordCode('valid_code'); + + expect($result)->toBeFalse(); +}); + +it('generates a new activation code and sets activated_at to null', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('newQuery->where->count')->andReturn(0); + $user->shouldReceive('newQuery->update')->once(); + + $activationCode = $user->getActivationCode(); + + expect($activationCode)->toBe($user->activation_code) + ->and($user->activated_at)->toBeNull(); +}); + +it('handles activation code collision by generating a new one', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('newQuery->where->count')->andReturn(1, 1, 0); + $user->shouldReceive('newQuery->update')->once(); + + $activationCode = $user->getActivationCode(); + + expect($activationCode)->not->toBeNull() + ->and($user->activation_code)->toBe($activationCode); +}); + +it('throws SystemException when user is already active', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->is_activated = true; + + expect(fn() => $user->completeActivation('some_code'))->toThrow(SystemException::class, 'User is already active!'); +}); + +it('returns true when activation code is correct and user is not active', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->is_activated = false; + $user->activation_code = 'valid_code'; + $user->shouldReceive('newQuery->update')->andReturn(true); + + $result = $user->completeActivation('valid_code'); + + expect($result)->toBeTrue(); +}); + +it('returns false when activation code is incorrect and user is not active', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->is_activated = false; + $user->activation_code = 'valid_code'; + + $result = $user->completeActivation('invalid_code'); + + expect($result)->toBeFalse(); +}); + +it('returns latest notifications', function() { + $user = Mockery::mock(User::class)->makePartial(); + $query = Mockery::mock(MorphMany::class); + $query->shouldReceive('latest')->andReturnSelf(); + $user->shouldReceive('morphMany')->with(Notification::class, 'notifiable')->andReturn($query); + + $result = $user->notifications(); + + expect($result)->toBe($query); +}); diff --git a/tests/Auth/UserGuardTest.php b/tests/Auth/UserGuardTest.php new file mode 100644 index 0000000..1fa92ef --- /dev/null +++ b/tests/Auth/UserGuardTest.php @@ -0,0 +1,103 @@ +makePartial(); + $guard->shouldReceive('check')->andReturn(true); + + $result = $guard->isLogged(); + + expect($result)->toBeTrue(); +}); + +it('checks if user is super user', function() { + $user = Mockery::mock(User::class)->makePartial(); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $user->shouldReceive('isSuperUser')->andReturn(true); + $guard->shouldReceive('user')->andReturn($user); + + $result = $guard->isSuperUser(); + + expect($result)->toBeTrue(); +}); + +it('returns staff instance', function() { + $user = Mockery::mock(User::class)->makePartial(); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->shouldReceive('user')->andReturn($user); + + $result = $guard->staff(); + + expect($result)->toBe($user); +}); + +it('returns user locations', function() { + $locations = Mockery::mock(Collection::class); + $user = Mockery::mock(User::class)->makePartial(); + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->shouldReceive('user')->andReturn($user); + $user->shouldReceive('getAttribute')->with('locations')->andReturn($locations); + + $result = $guard->locations(); + + expect($result)->toBe($locations); +}); + +it('returns user id', function() { + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->shouldReceive('id')->andReturn(1); + + $result = $guard->getId(); + + expect($result)->toBe(1); +}); + +it('returns user name', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->username = 'john_doe'; + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->setUser($user); + + $result = $guard->getUserName(); + + expect($result)->toBe('john_doe'); +}); + +it('returns user email', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->email = 'john.doe@example.com'; + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->setUser($user); + + $result = $guard->getUserEmail(); + + expect($result)->toBe('john.doe@example.com'); +}); + +it('returns staff name', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->name = 'John Doe'; + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->setUser($user); + + $result = $guard->getStaffName(); + + expect($result)->toBe('John Doe'); +}); + +it('returns staff email', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->email = 'john.doe@example.com'; + $guard = Mockery::mock(UserGuard::class)->makePartial(); + $guard->setUser($user); + + $result = $guard->getStaffEmail(); + + expect($result)->toBe('john.doe@example.com'); +}); diff --git a/tests/Auth/UserProviderTest.php b/tests/Auth/UserProviderTest.php new file mode 100644 index 0000000..409d9e0 --- /dev/null +++ b/tests/Auth/UserProviderTest.php @@ -0,0 +1,156 @@ +create(['status' => 1]); + $provider = new UserProvider(['model' => User::class]); + + $result = $provider->retrieveById($user->getKey()); + + expect($result->getKey())->toBe($user->getKey()); +}); + +it('retrieves user by token', function() { + $user = User::factory()->create([ + 'remember_token' => 'token', + 'status' => 1, + ]); + + $provider = new UserProvider(['model' => User::class]); + + $result = $provider->retrieveByToken($user->getKey(), 'token'); + + expect($result->getKey())->toBe($user->getKey()); +}); + +it('updates remember token', function() { + $user = User::factory()->create(); + $provider = new UserProvider(['model' => User::class]); + + $provider->updateRememberToken($user, 'new_token'); + + expect($user->getRememberToken())->toBe('new_token'); +}); + +it('retrieves user by credentials', function() { + $email = 'test@example.com'; + $user = User::factory()->create(['email' => $email, 'status' => 1]); + $provider = new UserProvider(['model' => User::class]); + + $result = $provider->retrieveByCredentials(['email' => $email]); + + expect($result->getKey())->toBe($user->getKey()); +}); + +it('validates user credentials', function() { + $user = User::factory()->create(['status' => 1]); + $provider = Mockery::mock(UserProvider::class)->makePartial(); + $provider->shouldReceive('hasShaPassword')->andReturn(false); + Hash::shouldReceive('check')->with('password', $user->getAuthPassword())->andReturn(true); + + $result = $provider->validateCredentials($user, ['password' => 'password']); + + expect($result)->toBeTrue(); +}); + +it('validates user credentials converts SHA1 passwords to bcrypt', function() { + $user = Mockery::mock(User::class); + $provider = Mockery::mock(UserProvider::class)->makePartial(); + $provider->shouldReceive('hasShaPassword')->andReturn(true); + Hash::shouldReceive('check')->with('password', 'hashed_password')->andReturn(true); + $user->shouldReceive('getAuthPassword')->andReturn('hashed_password'); + $user->shouldReceive('getAuthPasswordName')->andReturn('password'); + Hash::shouldReceive('make')->with('password')->andReturn('hashed_password'); + $user->shouldReceive('forceFill')->with([ + 'password' => 'hashed_password', + 'salt' => null, + ])->andReturnSelf()->once(); + $user->shouldReceive('save')->once(); + + $result = $provider->validateCredentials($user, ['password' => 'password']); + + expect($result)->toBeTrue(); +}); + +it('does not validates user credentials when password is missing', function() { + $user = Mockery::mock(User::class); + $provider = Mockery::mock(UserProvider::class)->makePartial(); + + $result = $provider->validateCredentials($user, ['password' => null]); + + expect($result)->toBeFalse(); +}); +it('returns false if user salt is null', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->salt = null; + + $provider = new UserProvider(['model' => User::class]); + + $result = $provider->hasShaPassword($user, ['password' => 'password']); + + expect($result)->toBeFalse(); +}); + +it('returns true if user password matches SHA1 hash', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->salt = 'random_salt'; + $user->shouldReceive('extendableGet') + ->with('password') + ->andReturn(sha1('random_salt'.sha1('random_salt'.sha1('password')))); + + $provider = new UserProvider(['model' => User::class]); + + $result = $provider->hasShaPassword($user, ['password' => 'password']); + + expect($result)->toBeTrue(); +}); + +it('returns false if user password does not match SHA1 hash', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->salt = 'random_salt'; + $user->password = sha1('random_salt'.sha1('random_salt'.sha1('wrong_password'))); + + $provider = new UserProvider(['model' => User::class]); + + $result = $provider->hasShaPassword($user, ['password' => 'password']); + + expect($result)->toBeFalse(); +}); + +it('rehashes password if required', function() { + $user = Mockery::mock(User::class); + $provider = new UserProvider(['model' => User::class]); + $user->shouldReceive('getAuthPassword')->andReturn('password'); + $user->shouldReceive('getAuthPasswordName')->andReturn('password'); + $user->shouldReceive('forceFill')->with(['password' => 'hashed_password'])->andReturnSelf()->once(); + $user->shouldReceive('save')->once(); + Hash::shouldReceive('needsRehash')->with('password')->andReturn(true)->once(); + Hash::shouldReceive('make')->with('password')->andReturn('hashed_password'); + + $provider->rehashPasswordIfRequired($user, ['password' => 'password']); +}); + +it('does not rehashes password', function() { + $user = Mockery::mock(User::class); + $provider = new UserProvider(['model' => User::class]); + $user->shouldReceive('getAuthPassword')->andReturn('password'); + Hash::shouldReceive('needsRehash')->with('password')->andReturn(false)->once(); + + $provider->rehashPasswordIfRequired($user, ['password' => 'password']); +}); + +it('registers a new user', function() { + $user = Mockery::mock(User::class); + $provider = Mockery::mock(UserProvider::class)->makePartial()->shouldAllowMockingProtectedMethods(); + $provider->shouldReceive('createModel->register')->with(['email' => 'test@example.com'], true)->andReturn($user); + + $result = $provider->register(['email' => 'test@example.com'], true); + + expect($result)->toBe($user); +}); diff --git a/tests/AutomationRules/Conditions/CustomerAttributeTest.php b/tests/AutomationRules/Conditions/CustomerAttributeTest.php new file mode 100644 index 0000000..4b34dfc --- /dev/null +++ b/tests/AutomationRules/Conditions/CustomerAttributeTest.php @@ -0,0 +1,68 @@ +conditionDetails(); + + expect($result)->toBe([ + 'name' => 'Customer attribute', + 'description' => 'Customer attributes', + ]); +}); + +it('defines correct model attributes', function() { + $condition = new CustomerAttribute; + + $result = $condition->defineModelAttributes(); + + expect($result)->toBe([ + 'first_name' => [ + 'label' => 'First Name', + ], + 'last_name' => [ + 'label' => 'Last Name', + ], + 'telephone' => [ + 'label' => 'Telephone', + ], + 'email' => [ + 'label' => 'Email address', + ], + ]); +}); + +it('returns true when customer attribute condition is met', function() { + $customer = Mockery::mock(Customer::class); + $condition = Mockery::mock(CustomerAttribute::class)->makePartial(); + $condition->shouldReceive('evalIsTrue')->with($customer)->andReturn(true); + + $params = ['customer' => $customer]; + $result = $condition->isTrue($params); + + expect($result)->toBeTrue(); +}); + +it('throws exception when customer object is not found in parameters', function() { + $condition = Mockery::mock(CustomerAttribute::class)->makePartial(); + + $params = []; + $exception = null; + + try { + $condition->isTrue($params); + } catch (AutomationException $e) { + $exception = $e; + } + + expect($exception)->not->toBeNull() + ->and($exception->getMessage()) + ->toBe('Error evaluating the customer attribute condition: the customer object is not found in the condition parameters.'); +}); diff --git a/tests/AutomationRules/Events/CustomerRegisteredTest.php b/tests/AutomationRules/Events/CustomerRegisteredTest.php new file mode 100644 index 0000000..fb7b6d6 --- /dev/null +++ b/tests/AutomationRules/Events/CustomerRegisteredTest.php @@ -0,0 +1,24 @@ +eventDetails(); + + expect($result)->toBe([ + 'name' => 'Customer Registered Event', + 'description' => 'When a customer registers', + 'group' => 'customer', + ]); +}); + +it('makes params from event', function() { + $args = ['customer' => 'John Doe']; + $result = CustomerRegistered::makeParamsFromEvent($args); + + expect($result)->toBe($args); +}); diff --git a/tests/Classes/BladeExtensionTest.php b/tests/Classes/BladeExtensionTest.php new file mode 100644 index 0000000..bb4c3a4 --- /dev/null +++ b/tests/Classes/BladeExtensionTest.php @@ -0,0 +1,45 @@ +with('mainauth', Mockery::type('array'))->once(); + Blade::shouldReceive('directive')->with('endmainauth', Mockery::type('array'))->once(); + Blade::shouldReceive('directive')->with('adminauth', Mockery::type('array'))->once(); + Blade::shouldReceive('directive')->with('endadminauth', Mockery::type('array'))->once(); + + $extension = new BladeExtension; + $extension->register(); +}); + +it('compiles mainauth directive', function() { + $extension = new BladeExtension; + $result = $extension->compilesMainAuth(null); + + expect($result)->toBe(""); +}); + +it('compiles adminauth directive', function() { + $extension = new BladeExtension; + $result = $extension->compilesAdminAuth(null); + + expect($result)->toBe(""); +}); + +it('compiles endmainauth directive', function() { + $extension = new BladeExtension; + $result = $extension->compilesEndMainAuth(); + + expect($result)->toBe(''); +}); + +it('compiles endadminauth directive', function() { + $extension = new BladeExtension; + $result = $extension->compilesEndAdminAuth(); + + expect($result)->toBe(''); +}); diff --git a/tests/Classes/NotificationTest.php b/tests/Classes/NotificationTest.php new file mode 100644 index 0000000..ad4e4a7 --- /dev/null +++ b/tests/Classes/NotificationTest.php @@ -0,0 +1,102 @@ +title('Test Title') + ->message('Test Message') + ->subject(new User); + + expect($notification->getTitle())->toBe('Test Title') + ->and($notification->getMessage())->toBe('Test Message') + ->and($notification->getRecipients())->toBeArray(); +}); + +it('broadcasts notification to users', function() { + $user = Mockery::mock(User::class); + $user->shouldReceive('notify')->once(); + + $notification = new Notification; + $notification->broadcast([$user]); +}); + +it('returns correct channels when broadcast is configured', function() { + Settings::set([ + 'app_id' => 'foo', + 'key' => 'foo', + 'secret' => 'foo', + ]); + + $notification = new Notification; + $channels = $notification->via(new stdClass); + + expect($channels)->toContain('database', 'broadcast'); + Settings::clearInternalCache(); +}); + +it('returns correct channels when broadcast is not configured', function() { + $notification = new Notification; + $channels = $notification->via(new stdClass); + + expect($channels)->toContain('database') + ->and($channels)->not->toContain('broadcast'); +}); + +it('returns correct database notification data', function() { + $notification = (new Notification) + ->title('Test Title') + ->message('Test Message') + ->url('http://example.com') + ->icon('icon.png') + ->iconColor('blue'); + + $data = $notification->toDatabase(new stdClass); + + expect($data)->toBe([ + 'title' => 'Test Title', + 'icon' => 'icon.png', + 'iconColor' => 'blue', + 'url' => 'http://example.com', + 'message' => 'Test Message', + ]); +}); + +it('returns correct broadcast notification data', function() { + $notification = (new Notification) + ->title('Test Title') + ->message('Test Message') + ->url('http://example.com') + ->icon('icon.png') + ->iconColor('blue'); + + $broadcastMessage = $notification->toBroadcast(new stdClass); + + expect($broadcastMessage->data)->toBe([ + 'title' => 'Test Title', + 'icon' => 'icon.png', + 'iconColor' => 'blue', + 'url' => 'http://example.com', + 'message' => 'Test Message', + ]); +}); + +it('returns correct alias for database type', function() { + $notification = new Notification; + $alias = $notification->databaseType(new stdClass); + + expect($alias)->toBe(Notification::class); +}); + +it('returns correct alias for broadcast type', function() { + $notification = new Notification; + $alias = $notification->broadcastType(); + + expect($alias)->toBe(Notification::class); +}); diff --git a/tests/Classes/PermissionManagerTest.php b/tests/Classes/PermissionManagerTest.php new file mode 100644 index 0000000..9ef9859 --- /dev/null +++ b/tests/Classes/PermissionManagerTest.php @@ -0,0 +1,141 @@ +shouldReceive('getRegistrationMethodValues')->with('registerPermissions')->andReturn([]); + app()->instance(ExtensionManager::class, $extensionManager); + $permissionManager = new PermissionManager; + + $result = $permissionManager->listPermissions(); + + expect($result)->toBeEmpty(); +}); + +it('returns registered permissions', function() { + $extensionManager = Mockery::mock(ExtensionManager::class); + $extensionManager->shouldReceive('getRegistrationMethodValues')->with('registerPermissions')->andReturn([ + 'owner' => 'not an array', + ]); + app()->instance(ExtensionManager::class, $extensionManager); + $permissionManager = new PermissionManager; + $permissionManager->registerPermissions('testOwner', [ + 'test.another-code' => [ + 'label' => 'Test Another Permission', + 'group' => 'Test Group', + 'priority' => 99, + ], + 'test.code' => [ + 'label' => 'Test Permission', + 'group' => 'Test Group', + 'priority' => 9, + ], + ]); + + $result = $permissionManager->listPermissions(); + + expect($result)->toBeGreaterThanOrEqual(1) + ->and($result[0]->code)->toBe('test.code') + ->and($result[0]->label)->toBe('Test Permission') + ->and($result[0]->group)->toBe('Test Group'); +}); + +it('returns registered permissions from extensions', function() { + $permissionManager = new PermissionManager; + + $result = $permissionManager->listPermissions(); + + expect($result)->not->toBeEmpty(); +}); + +it('returns grouped permissions', function() { + $permissionManager = new PermissionManager; + $permissionManager->registerPermissions('testOwner', [ + 'test.code' => [ + 'label' => 'Test Permission', + 'group' => 'Test Group', + ], + 'test.another-code' => [ + 'label' => 'Test Permission', + ], + ]); + + $result = $permissionManager->listGroupedPermissions(); + + expect($result)->toHaveKey('test group') + ->and($result['test group'])->toHaveCount(1) + ->and($result['test group'][0]->code)->toBe('test.code'); +}); + +it('checks permission with starts with wildcard', function() { + $permissionManager = new PermissionManager; + $permissions = ['test.*' => 1]; + + $result = $permissionManager->checkPermission($permissions, ['test.view'], false); + + expect($result)->toBeTrue(); +}); + +it('checks permission with ends with wildcard', function() { + $permissionManager = new PermissionManager; + $permissions = ['*.view' => 1]; + + $result = $permissionManager->checkPermission($permissions, ['test.view'], false); + + expect($result)->toBeTrue(); +}); + +it('checks permission with exact match', function() { + $permissionManager = new PermissionManager; + $permissions = ['test.view' => 1]; + + $result = $permissionManager->checkPermission($permissions, ['test.view'], false); + + expect($result)->toBeTrue(); +}); + +it('returns false when permission is not matched', function() { + $permissionManager = new PermissionManager; + $permissions = ['test.view' => 1]; + + $result = $permissionManager->checkPermission($permissions, ['test.edit'], true); + + expect($result)->toBeFalse(); + + $result = $permissionManager->checkPermission($permissions, ['test.edit'], false); + + expect($result)->toBeFalse(); +}); + +it('executes registered callback functions', function() { + $extensionManager = Mockery::mock(ExtensionManager::class); + $extensionManager->shouldReceive('getRegistrationMethodValues')->with('registerPermissions')->andReturn([]); + app()->instance(ExtensionManager::class, $extensionManager); + $permissionManager = new PermissionManager; + $callback = function($manager) { + $manager->registerPermissions('testOwner', [ + 'test.code' => [ + 'label' => 'Test Permission', + 'group' => 'Test Group', + ], + 'test.code' => [ + 'description' => 'Test Permission', + 'group' => 'Test Group', + ], + ]); + }; + + $permissionManager->registerCallback($callback); + $permissionManager->listPermissions(); + + $permissions = $permissionManager->listPermissions(); + expect($permissions)->toHaveCount(1) + ->and($permissions[0]->code)->toBe('test.code') + ->and($permissions[0]->label)->toBe('Test Permission') + ->and($permissions[0]->group)->toBe('Test Group'); +}); diff --git a/tests/Classes/UserStateTest.php b/tests/Classes/UserStateTest.php new file mode 100644 index 0000000..517b5fe --- /dev/null +++ b/tests/Classes/UserStateTest.php @@ -0,0 +1,118 @@ +superUser()->create(), 'igniter-admin'); + $userState = UserState::forUser(); + $userState->updateState(UserState::AWAY_STATUS, '', 0); + + expect($userState->isAway())->toBeTrue() + ->and($userState->isOnline())->toBeFalse() + ->and($userState->isIdle())->toBeFalse(); +}); + +it('returns false when user is online', function() { + actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $userState = UserState::forUser(); + $userState->updateState(UserState::ONLINE_STATUS, '', 0); + + expect($userState->isAway())->toBeFalse() + ->and($userState->isOnline())->toBeTrue() + ->and($userState->isIdle())->toBeFalse(); +}); + +it('returns true when user is idle', function() { + actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $userState = UserState::forUser(); + $userState->updateState(UserState::BACK_SOON_STATUS, '', 0); + + expect($userState->isOnline())->toBeFalse() + ->and($userState->isIdle())->toBeTrue(); +}); + +it('returns true when user has custom status', function() { + actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $userState = UserState::forUser(); + $userState->updateState(UserState::CUSTOM_STATUS, '', 0); + + expect($userState->isAway())->toBeTrue(); +}); + +it('returns false when user has custom status', function() { + $userState = UserState::forUser(); + + expect($userState->isAway())->toBeFalse(); +}); + +it('returns correct status name for custom status', function() { + actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $userState = UserState::forUser(); + $userState->updateState(UserState::CUSTOM_STATUS, 'Busy', 0); + + expect($userState->getStatusName())->toBe('Busy'); +}); + +it('returns correct status name for predefined status', function() { + actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $userState = UserState::forUser(); + $userState->updateState(UserState::BACK_SOON_STATUS, '', 0); + + expect($userState->getStatusName())->toBe('igniter.user::default.staff_status.text_back_soon'); +}); + +it('returns correct clear after time for custom status', function() { + actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $this->travelTo('2021-01-01 12:00:00'); + $userState = UserState::forUser(); + $userState->updateState(UserState::CUSTOM_STATUS, 'Busy', 30); + + $clearAfterAt = $userState->getClearAfterAt(); + + expect($clearAfterAt)->not->toBeNull() + ->and((int)now()->diffInMinutes($clearAfterAt))->toBe(30); +}); + +it('returns null clear after time for non-custom status', function() { + actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $userState = UserState::forUser(); + $userState->updateState(UserState::ONLINE_STATUS, '', 30); + + expect($userState->getClearAfterAt())->toBeNull(); +}); + +it('returns updated at time when set', function() { + actingAs(User::factory()->superUser()->create(), 'igniter-admin'); + $userState = UserState::forUser(); + $userState->updateState(UserState::ONLINE_STATUS, '', 0); + + $updatedAt = $userState->getUpdatedAt(); + + expect($updatedAt)->not->toBeNull() + ->and($updatedAt)->toBeInstanceOf(Carbon::class); +}); + +it('returns null when updated at time is not set', function() { + $userState = UserState::forUser(); + + $updatedAt = $userState->getUpdatedAt(); + + expect($updatedAt)->toBeNull(); +}); + +it('returns correct clear after minutes dropdown options', function() { + $options = UserState::getClearAfterMinutesDropdownOptions(); + + expect($options)->toBe([ + 1440 => 'igniter.user::default.staff_status.text_clear_tomorrow', + 240 => 'igniter.user::default.staff_status.text_clear_hours', + 30 => 'igniter.user::default.staff_status.text_clear_minutes', + 0 => 'igniter.user::default.staff_status.text_dont_clear', + ]); +}); diff --git a/tests/Console/Commands/AllocatorCommandTest.php b/tests/Console/Commands/AllocatorCommandTest.php new file mode 100644 index 0000000..fb9e8b1 --- /dev/null +++ b/tests/Console/Commands/AllocatorCommandTest.php @@ -0,0 +1,51 @@ + true, 'slot2' => true], 'prefs'); + Settings::set('allocator_slot_size', 2, 'prefs'); + + (new AllocatorCommand)->handle(); + expect(true)->toBeTrue(); +}); + +it('dispatches jobs when there are available slots', function() { + Settings::set('allocator_slots', ['slot1' => true, 'slot2' => true], 'prefs'); + Settings::set('allocator_slot_size', 10, 'prefs'); + + (new AllocatorCommand)->handle(); + + expect(true)->toBeTrue(); +}); + +it('adds a single slot', function() { + Settings::set('allocator_slots', [], 'prefs'); + + AllocatorCommand::addSlot('slot1'); + + $slots = Settings::get('allocator_slots', null, 'prefs'); + expect($slots)->toHaveKey('slot1'); +}); + +it('adds multiple slots', function() { + Settings::set('allocator_slots', [], 'prefs'); + + AllocatorCommand::addSlot(['slot1', 'slot2']); + + $slots = Settings::get('allocator_slots', null, 'prefs'); + expect($slots)->toHaveKeys(['slot1', 'slot2']); +}); + +it('removes a slot', function() { + Settings::set('allocator_slots', ['slot1' => true, 'slot2' => true], 'prefs'); + + AllocatorCommand::removeSlot('slot1'); + + $slots = Settings::get('allocator_slots', null, 'prefs'); + expect($slots)->not->toHaveKey('slot1') + ->and($slots)->toHaveKey('slot2'); +}); diff --git a/tests/Console/Commands/ClearUserStateCommandTest.php b/tests/Console/Commands/ClearUserStateCommandTest.php new file mode 100644 index 0000000..00cabc6 --- /dev/null +++ b/tests/Console/Commands/ClearUserStateCommandTest.php @@ -0,0 +1,69 @@ + UserState::USER_PREFERENCE_KEY, + 'value' => [ + 'status' => UserState::CUSTOM_STATUS, + 'clearAfterMinutes' => 10, + 'updatedAt' => now()->subMinutes(15)->toDateTimeString(), + ], + ]); + + $this->artisan('igniter:user-state-clear'); + $preference->refresh(); + + expect($preference->value)->toBe([ + 'status' => UserState::ONLINE_STATUS, + 'awayMessage' => null, + 'updatedAt' => null, + 'clearAfterMinutes' => 0, + ]); +}); + +it('does not clear non-expired custom away status', function() { + $preference = UserPreference::create([ + 'item' => UserState::USER_PREFERENCE_KEY, + 'value' => [ + 'status' => UserState::CUSTOM_STATUS, + 'clearAfterMinutes' => 10, + 'updatedAt' => now()->subMinutes(5)->toDateTimeString(), + ], + ]); + + $this->artisan('igniter:user-state-clear'); + + $preference->refresh(); + expect($preference->value)->not->toBe([ + 'status' => 1, + 'awayMessage' => null, + 'updatedAt' => null, + 'clearAfterMinutes' => 0, + ]); +}); + +it('does not clear status with clearAfterMinutes set to zero', function() { + $preference = UserPreference::create([ + 'item' => UserState::USER_PREFERENCE_KEY, + 'value' => [ + 'status' => UserState::CUSTOM_STATUS, + 'clearAfterMinutes' => 0, + 'updatedAt' => null, + ], + ]); + + $this->artisan('igniter:user-state-clear'); + + $preference->refresh(); + expect($preference->value)->not->toBe([ + 'status' => 1, + 'awayMessage' => null, + 'updatedAt' => null, + 'clearAfterMinutes' => 0, + ]); +}); diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 6503bec..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,5 +0,0 @@ -toBeTrue(); -}); diff --git a/tests/ExtensionTest.php b/tests/ExtensionTest.php new file mode 100644 index 0000000..5aecc8e --- /dev/null +++ b/tests/ExtensionTest.php @@ -0,0 +1,208 @@ +extension = new Extension(app()); +}); + +it('listens to user register event and broadcasts notification', function() { + Event::fake(); + $customer = Mockery::mock(Customer::class)->makePartial(); + $data = ['key' => 'value']; + + Event::dispatch('igniter.user.register', [$customer, $data]); + + Event::assertDispatched('igniter.user.register', function($event, $payload) use ($customer, $data) { + return $payload[0] === $customer && $payload[1] === $data; + }); +}); + +it('extends location model with users relation', function() { + $this->extension->boot(); + + expect((new Location)->relation['morphedByMany']['users'])->toBe([User::class, 'name' => 'locationable']); +}); + +it('registers endBody hook for admin impersonate banner', function() { + Template::shouldReceive('registerHook')->once()->with('endBody', Mockery::on(function($callback) { + $view = $callback(); + expect($view->getName())->toBe('igniter.user::_partials.admin_impersonate_banner'); + + return true; + })); + + $this->extension->boot(); +}); + +it('listens to NotificationSent event and deletes old notifications', function() { + $event = Mockery::mock(NotificationSent::class)->makePartial(); + $event->response = $notification = Mockery::mock(Notification::class); + $notification->shouldReceive('getKey')->andReturn(1); + $event->notification = new class implements StickyNotification + { + public function databaseType() + { + return 'type'; + } + }; + $event->notifiable = $notifiable = Mockery::mock(User::class); + $notifiable->shouldReceive('notifications->where->where->delete')->andReturnSelf()->once(); + Event::shouldReceive('listen')->with(NotificationSent::class, Mockery::on(function($callback) use ($event) { + $callback($event); + + return true; + }))->once(); + Event::shouldReceive('listen')->with('igniter.user.beforeThrottleRequest', Mockery::any())->once(); + Event::shouldReceive('listen')->with('igniter.user.register', Mockery::any())->once(); + + $this->extension->boot(); +}); + +it('listens to igniter.user.register event and send CustomerRegisteredNotification', function() { + \Illuminate\Support\Facades\Notification::fake(); + + Event::shouldReceive('listen')->with(NotificationSent::class, Mockery::any())->once(); + Event::shouldReceive('listen')->with('igniter.user.beforeThrottleRequest', Mockery::any())->once(); + Event::shouldReceive('listen')->with('igniter.user.register', Mockery::on(function($callback) { + $customer = Mockery::mock(Customer::class)->makePartial(); + $callback($customer, []); + + return true; + }))->once(); + + $this->extension->boot(); +}); + +it('registers customer statistics card', function() { + $this->extension->boot(); + + $reflection = new \ReflectionClass(Statistics::class); + $method = $reflection->getMethod('listCards'); + $method->setAccessible(true); + $cards = $method->invoke(new Statistics(resolve(Menus::class))); + + expect($cards)->toHaveKey('customer') + ->and($cards['customer']['label'])->toBe('lang:igniter::admin.dashboard.text_total_customer') + ->and($cards['customer']['valueFrom']('customer', null, null, function($query) { + return $query; + }))->toBe(0); +}); + +it('registers user system settings', function() { + $this->extension->registerSystemSettings(); + $settingsItems = (new Settings)->listSettingItems(); + $settingsItem = collect($settingsItems['core'])->firstWhere('code', 'user'); + + expect($settingsItem->label)->toBe('lang:igniter.user::default.text_tab_user') + ->and($settingsItem->description)->toBe('lang:igniter.user::default.text_tab_desc_user') + ->and($settingsItem->icon)->toBe('fa fa-users-gear') + ->and($settingsItem->priority)->toBe(2) + ->and($settingsItem->permission)->toBe(['Site.Settings']) + ->and($settingsItem->url)->toBe(admin_url('settings/edit/user')) + ->and($settingsItem->form)->toBe('igniter.user::/models/usersettings') + ->and($settingsItem->request)->toBe(UserSettingsRequest::class); +}); + +it('registers global event parameters when EventManager class exists', function() { + $eventManager = Mockery::mock(EventManager::class); + app()->instance(EventManager::class, $eventManager); + $eventManager->shouldReceive('registerCallback')->once()->andReturnUsing(function($callback) use ($eventManager) { + $eventManager->shouldReceive('registerGlobalParams')->with(['customer' => Auth::customer()]); + $callback($eventManager); + }); + + $this->extension->register(); +}); + +it('registers user panel and notifications admin menus when running in admin', function() { + $request = Mockery::mock(Request::class); + $request->shouldReceive('setUserResolver')->andReturnNull(); + $request->shouldReceive('getScheme')->andReturn('https'); + $request->shouldReceive('root')->andReturn('localhost'); + $request->shouldReceive('route')->andReturnNull(); + $request->shouldReceive('path')->andReturn('admin/dashboard'); + app()->instance('request', $request); + + $this->extension->boot(); + $menuItems = AdminMenu::getMainItems(); + + expect($menuItems['notifications'])->not->toBeNull() + ->and($menuItems['user'])->not->toBeNull(); +}); + +it('does not register user panel and notifications admin menus when not running in admin', function() { + $this->extension->boot(); + $menuItems = AdminMenu::getMainItems(); + + expect($menuItems)->not->toHaveKeys(['notifications', 'user']); +}); + +it('does not define routes when routes are cached', function() { + $app = Mockery::mock(Application::class)->makePartial(); + $app->shouldReceive('routesAreCached')->andReturn(true); + Container::setInstance($app); + Route::shouldReceive('group')->never(); + + $reflection = new \ReflectionClass(Extension::class); + $method = $reflection->getMethod('defineRoutes'); + $method->setAccessible(true); + $method->invoke($this->extension); +}); + +it('configures rate limiter', function() { + RateLimiter::shouldReceive('for')->with('web', Mockery::on(function($callback) { + $request = Mockery::mock(Request::class); + $request->shouldReceive('ip')->andReturn('127.0.0.1'); + $request->shouldReceive('user')->andReturnNull(); + expect($callback($request))->toBeInstanceOf(Limit::class); + + return true; + }))->once(); + + $reflection = new \ReflectionClass(Extension::class); + $method = $reflection->getMethod('configureRateLimiting'); + $method->setAccessible(true); + $method->invoke($this->extension); +}); + +it('extends dashboard charts datasets', function() { + $this->extension->boot(); + + $reflection = new \ReflectionClass(Charts::class); + $method = $reflection->getMethod('listSets'); + $method->setAccessible(true); + $result = $method->invoke(new Charts(resolve(Menus::class))); + $datasets = $result['reports']['sets']; + + expect($datasets)->toHaveKey('customers') + ->and($datasets['customers']['label'])->toBe('lang:igniter.user::default.text_charts_customers') + ->and($datasets['customers']['color'])->toBe('#4DB6AC') + ->and($datasets['customers']['model'])->toBe(Customer::class) + ->and($datasets['customers']['column'])->toBe('created_at'); +}); diff --git a/tests/FormWidgets/PermissionEditorTest.php b/tests/FormWidgets/PermissionEditorTest.php new file mode 100644 index 0000000..969b282 --- /dev/null +++ b/tests/FormWidgets/PermissionEditorTest.php @@ -0,0 +1,48 @@ +model = Mockery::mock(Model::class)->makePartial(); + $this->formField = new FormField('testField', 'Label'); + $this->permissionEditor = new PermissionEditor(resolve(Menus::class), $this->formField, ['model' => $this->model]); +}); + +it('initializes with correct config', function() { + $this->permissionEditor->initialize(); + + expect($this->permissionEditor->mode)->toBeNull(); +}); + +it('renders the correct partial', function() { + $this->permissionEditor->initialize(); + + $partial = $this->permissionEditor->render(); + + expect($partial)->toBeString(); +}); + +it('prepares variables correctly', function() { + $this->permissionEditor->prepareVars(); + + expect($this->permissionEditor->vars['groupedPermissions'])->toBeArray() + ->and($this->permissionEditor->vars['checkedPermissions'])->toBe([]) + ->and($this->permissionEditor->vars['field'])->toBe($this->formField); +}); + +it('loads the correct assets', function() { + Assets::shouldReceive('addJs') + ->with(Mockery::on(function($url) { + return str_contains($url, 'permissioneditor.js'); + }), 'permissioneditor-js') + ->once(); + + $this->permissionEditor->loadAssets(); +}); diff --git a/tests/Http/Actions/AssigneeControllerTest.php b/tests/Http/Actions/AssigneeControllerTest.php new file mode 100644 index 0000000..bcbbef4 --- /dev/null +++ b/tests/Http/Actions/AssigneeControllerTest.php @@ -0,0 +1,146 @@ +makePartial(); + $user->shouldReceive('extendableGet')->with('groups')->andReturn(collect([['user_group_id' => 1], ['user_group_id' => 2]])); + $user->shouldReceive('hasGlobalAssignableScope')->andReturn(false); + $user->shouldReceive('hasRestrictedAssignableScope')->andReturn(true); + $user->shouldReceive('getKey')->andReturn(1); + $user->shouldReceive('groups->pluck->all')->andReturn([1, 2]); + $controller->setUser($user); + $query->shouldReceive('whereInAssignToGroup')->with([1, 2])->once(); + $query->shouldReceive('whereAssignTo')->with(1)->once(); + + $assigneeController = new AssigneeController($controller); + + $assigneeController->assigneeApplyScope($query); +}); + +it('does not apply scope on list query when user has global scope', function() { + $controller = resolve(Menus::class); + $user = Mockery::mock(User::class)->makePartial(); + $query = Mockery::mock(Builder::class); + $user->shouldReceive('hasGlobalAssignableScope')->andReturn(true); + $controller->setUser($user); + $query->shouldReceive('whereInAssignToGroup')->never(); + $query->shouldReceive('whereAssignTo')->never(); + + $assigneeController = new AssigneeController($controller); + + $assigneeController->assigneeApplyScope($query); +}); + +it('removes delete button from toolbar when user does not have global scope', function() { + $controller = resolve(Menus::class); + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('hasGlobalAssignableScope')->andReturn(false); + $controller->setUser($user); + $toolbar = new Toolbar($controller); + $toolbar->allButtons = ['delete' => 'deleteWidget']; + $controller->widgets = ['toolbar' => $toolbar]; + + new AssigneeController($controller); + + $controller->fireEvent('controller.beforeRemap'); + $toolbar->fireEvent('toolbar.extendButtons', [$toolbar->allButtons]); + + expect($toolbar->allButtons)->not->toHaveKey('delete'); +}); + +it('does not remove delete button from toolbar when user has global scope', function() { + $controller = resolve(Menus::class); + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('hasGlobalAssignableScope')->andReturn(true); + $controller->setUser($user); + $toolbar = new Toolbar($controller); + $toolbar->allButtons = ['delete' => 'deleteWidget']; + $controller->widgets = ['toolbar' => $toolbar]; + + new AssigneeController($controller); + + $controller->fireEvent('controller.beforeRemap'); + $toolbar->fireEvent('toolbar.extendButtons', [$toolbar->allButtons]); + + expect($toolbar->allButtons)->toHaveKey('delete'); +}); + +it('assigns user to model when conditions are met', function() { + $controller = resolve(Menus::class); + $assignable = Mockery::mock(Order::class)->makePartial(); + $query = Mockery::mock(Builder::class); + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getKey')->andReturn(1); + $user->shouldReceive('hasGlobalAssignableScope')->andReturn(false); + $user->shouldReceive('hasRestrictedAssignableScope')->andReturn(true); + $controller->setUser($user); + $query->shouldReceive('getModel')->andReturn($user); + $query->shouldReceive('whereInAssignToGroup')->once(); + $query->shouldReceive('whereAssignTo')->with(1)->once(); + $assignable->shouldReceive('hasAssignToGroup')->andReturn(true); + $assignable->shouldReceive('hasAssignTo')->andReturnFalse()->once(); + $assignable->shouldReceive('extendableGet')->with('assignee_group')->andReturnSelf(); + $assignable->shouldReceive('autoAssignEnabled')->andReturnFalse()->once(); + $assignable->shouldReceive('cannotAssignToStaff')->with($user)->andReturnFalse()->once(); + $assignable->shouldReceive('assignTo')->with($user)->once(); + + $widget = new Form($controller, ['model' => $assignable]); + + new AssigneeController($controller); + $controller->fireEvent('controller.beforeRemap'); + + $controller->fireEvent('admin.controller.extendFormQuery', [$query]); + Event::dispatch('admin.form.extendFields', [$widget]); +}); + +it('applies scope on list query when extended', function() { + $controller = resolve(Menus::class); + $assignable = Mockery::mock(Order::class)->makePartial(); + $user = Mockery::mock(User::class)->makePartial(); + $query = Mockery::mock(Builder::class); + $query->shouldReceive('whereInAssignToGroup')->once(); + $query->shouldReceive('whereAssignTo')->with(1)->once(); + $query->shouldReceive('getModel')->andReturn($user); + $user->shouldReceive('getKey')->andReturn(1); + $user->shouldReceive('hasGlobalAssignableScope')->andReturn(false); + $user->shouldReceive('hasRestrictedAssignableScope')->andReturn(true); + $controller->setUser($user); + + $widget = new Form($controller, ['model' => $assignable]); + new AssigneeController($controller); + $controller->fireEvent('controller.beforeRemap'); + + Event::dispatch('admin.list.extendQuery', [$widget, $query]); +}); + +it('removes assignee scope when user has restricted scope', function() { + $controller = resolve(Menus::class); + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getKey')->andReturn(1); + $user->shouldReceive('hasGlobalAssignableScope')->andReturn(false); + $user->shouldReceive('hasRestrictedAssignableScope')->andReturn(true); + $controller->setUser($user); + + $widget = new Filter($controller); + $widget->scopes = ['assignee' => 'some_scope']; + new AssigneeController($controller); + $controller->fireEvent('controller.beforeRemap'); + + Event::dispatch('admin.filter.extendScopesBefore', [$widget]); + + expect($widget->scopes)->not->toHaveKey('assignee'); +}); diff --git a/tests/Http/Controllers/CustomerGroupsTest.php b/tests/Http/Controllers/CustomerGroupsTest.php new file mode 100644 index 0000000..4b90ccd --- /dev/null +++ b/tests/Http/Controllers/CustomerGroupsTest.php @@ -0,0 +1,109 @@ +get(route('igniter.user.customer_groups')) + ->assertOk(); +}); + +it('loads create customer group page', function() { + actingAsSuperUser() + ->get(route('igniter.user.customer_groups', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads edit customer group page', function() { + $customerGroup = CustomerGroup::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.customer_groups', ['slug' => 'edit/'.$customerGroup->getKey()])) + ->assertOk(); +}); + +it('loads customer group preview page', function() { + $customerGroup = CustomerGroup::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.customer_groups', ['slug' => 'preview/'.$customerGroup->getKey()])) + ->assertOk(); +}); + +it('sets a default customer group', function() { + CustomerGroup::$defaultModels = []; + $customerGroup = CustomerGroup::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.customer_groups'), [ + 'default' => $customerGroup->getKey(), + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSetDefault', + ]); + + expect(CustomerGroup::getDefaultKey())->toBe($customerGroup->getKey()); +}); + +it('creates customer group', function() { + actingAsSuperUser() + ->post(route('igniter.user.customer_groups', ['slug' => 'create']), [ + 'CustomerGroup' => [ + 'group_name' => 'Created Customer Group', + 'approval' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(CustomerGroup::where('group_name', 'Created Customer Group')->exists())->toBeTrue(); +}); + +it('updates customer group', function() { + $customerGroup = CustomerGroup::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.customer_groups', ['slug' => 'edit/'.$customerGroup->getKey()]), [ + 'CustomerGroup' => [ + 'group_name' => 'Updated Customer Group', + 'approval' => 0, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(CustomerGroup::where('group_name', 'Updated Customer Group')->exists())->toBeTrue(); +}); + +it('deletes customer group', function() { + $customerGroup = CustomerGroup::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.customer_groups', ['slug' => 'edit/'.$customerGroup->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(CustomerGroup::find($customerGroup->getKey()))->toBeNull(); +}); + +it('bulk deletes customer groups', function() { + $customerGroup = CustomerGroup::factory()->count(5)->create(); + $customerGroupIds = $customerGroup->pluck('customer_group_id')->all(); + + actingAsSuperUser() + ->post(route('igniter.user.customer_groups'), [ + 'checked' => $customerGroupIds, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(CustomerGroup::whereIn('customer_group_id', + $customerGroupIds, + )->exists())->toBeFalse(); +}); diff --git a/tests/Http/Controllers/CustomersTest.php b/tests/Http/Controllers/CustomersTest.php new file mode 100644 index 0000000..94b4fb0 --- /dev/null +++ b/tests/Http/Controllers/CustomersTest.php @@ -0,0 +1,161 @@ +get(route('igniter.user.customers')) + ->assertOk(); +}); + +it('loads create customer page', function() { + actingAsSuperUser() + ->get(route('igniter.user.customers', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads edit customer page', function() { + $customer = Customer::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.customers', ['slug' => 'edit/'.$customer->getKey()])) + ->assertOk(); +}); + +it('loads customer preview page', function() { + $customer = Customer::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.customers', ['slug' => 'preview/'.$customer->getKey()])) + ->assertOk(); +}); + +it('creates customer', function() { + actingAsSuperUser() + ->post(route('igniter.user.customers', ['slug' => 'create']), [ + 'Customer' => [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'user@example.com', + 'customer_group_id' => 1, + 'newsletter' => 1, + 'status' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Customer::where('email', 'user@example.com')->where('first_name', 'John')->exists())->toBeTrue(); +}); + +it('updates customer', function() { + $customer = Customer::factory()->create(['is_activated' => false]); + + actingAsSuperUser() + ->post(route('igniter.user.customers', ['slug' => 'edit/'.$customer->getKey()]), [ + 'Customer' => [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'user@example.com', + 'customer_group_id' => 1, + 'newsletter' => 1, + 'status' => 1, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(Customer::where('email', 'user@example.com')->where('first_name', 'John')->exists())->toBeTrue(); +}); + +it('deletes customer', function() { + $customer = Customer::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.customers', ['slug' => 'edit/'.$customer->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(Customer::find($customer->getKey()))->toBeNull(); +}); + +it('bulk deletes customers', function() { + $customer = Customer::factory()->count(5)->create(); + $customerIds = $customer->pluck('customer_id')->all(); + + actingAsSuperUser() + ->post(route('igniter.user.customers'), [ + 'checked' => $customerIds, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(Customer::whereIn('customer_id', $customerIds)->exists())->toBeFalse(); +}); + +it('throws exception when unauthorized to delete customer', function() { + $authGate = Mockery::mock(Gate::class); + $authGate->shouldReceive('inspect')->with('Admin.DeleteCustomers')->andReturnSelf(); + $authGate->shouldReceive('allowed')->andReturnFalse(); + app()->instance(Gate::class, $authGate); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(lang('igniter::admin.alert_user_restricted')); + + (new Customers)->index_onDelete(); +}); + +it('impersonates customer successfully', function() { + $customer = Customer::factory()->create([ + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + $authGate = Mockery::mock(Gate::class); + $authGate->shouldReceive('inspect')->with('Admin.ImpersonateCustomers')->andReturnSelf(); + $authGate->shouldReceive('allowed')->andReturnTrue(); + app()->instance(Gate::class, $authGate); + + (new Customers)->onImpersonate('edit', $customer->getKey()); + + expect(flash()->messages()->first()) + ->level->toBe('success') + ->message->toBe(sprintf(lang('igniter.user::default.customers.alert_impersonate_success'), 'John Doe')); +}); + +it('throws exception when unauthorized to impersonate customer', function() { + $customer = Customer::factory()->create([ + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + $authGate = Mockery::mock(Gate::class); + $authGate->shouldReceive('inspect')->with('Admin.ImpersonateCustomers')->andReturnSelf(); + $authGate->shouldReceive('allowed')->andReturnFalse(); + app()->instance(Gate::class, $authGate); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(lang('igniter.user::default.customers.alert_login_restricted')); + + (new Customers)->onImpersonate('edit', $customer->getKey()); +}); + +it('activates customer successfully', function() { + $customer = Customer::factory()->create(['is_activated' => false]); + + $response = (new Customers)->edit_onActivate('context', $customer->getKey()); + + expect($response)->toBeInstanceOf(RedirectResponse::class) + ->and(flash()->messages()->first()) + ->level->toBe('success') + ->message->toBe(lang('igniter.user::default.customers.alert_activation_success')); +}); diff --git a/tests/Http/Controllers/LoginTest.php b/tests/Http/Controllers/LoginTest.php new file mode 100644 index 0000000..93e448c --- /dev/null +++ b/tests/Http/Controllers/LoginTest.php @@ -0,0 +1,146 @@ +route = new Route('GET', 'login', ['as' => 'igniter.admin.login']); + $this->route->parameters = ['slug' => '']; +}); + +it('loads login page if not logged in', function() { + $this->route->action = []; + request()->setRouteResolver(fn() => $this->route); + + $response = (new Login)->index(); + + expect($response)->toBeInstanceOf(RedirectResponse::class); +}); + +it('redirects to dashboard if already logged in', function() { + AdminAuth::shouldReceive('isLogged')->andReturnTrue(); + AdminAuth::shouldReceive('isImpersonator')->andReturnFalse(); + AdminAuth::shouldReceive('check')->andReturnTrue(); + request()->setRouteResolver(fn() => $this->route); + + $response = (new Login)->index(); + + expect($response)->toBeInstanceOf(RedirectResponse::class) + ->and((new Login)->checkUser())->toBeTrue(); +}); + +it('renders login view if not logged in and on login route', function() { + request()->setRouteResolver(fn() => $this->route); + AdminAuth::shouldReceive('isLogged')->andReturnFalse(); + AdminAuth::shouldReceive('isImpersonator')->andReturnFalse(); + + $response = (new Login)->index(); + + expect($response)->toBeString(); +}); + +it('renders reset password view', function() { + AdminAuth::shouldReceive('isLogged')->andReturnFalse(); + AdminAuth::shouldReceive('isImpersonator')->andReturnFalse(); + + $response = (new Login)->reset(); + + expect($response)->toBeString(); +}); + +it('redirects reset password to dashboard if already logged in', function() { + AdminAuth::shouldReceive('isLogged')->andReturnTrue(); + + $response = (new Login)->reset(); + + expect($response)->toBeInstanceOf(RedirectResponse::class); +}); + +it('resets password successfully', function() { + $user = User::factory()->create(); + request()->request->set('email', $user->email); + AdminAuth::shouldReceive('isLogged')->andReturnFalse(); + + $response = (new Login)->onRequestResetPassword(); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/login') + ->and(flash()->messages()->first())->level->toBe('success') + ->message->toBe(lang('igniter::admin.login.alert_email_sent')); +}); + +it('fails to reset password with invalid code', function() { + AdminAuth::shouldReceive('isLogged')->andReturnFalse(); + AdminAuth::shouldReceive('isImpersonator')->andReturnFalse(); + request()->merge(['code' => 'invalid_code']); + + $response = (new Login)->reset(); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/login') + ->and(flash()->messages()->first())->level->toBe('danger') + ->message->toBe(lang('igniter::admin.login.alert_failed_reset')); +}); + +it('logs in successfully with valid credentials', function() { + $data = ['email' => 'test@example.com', 'password' => 'password']; + request()->request->add($data); + AdminAuth::shouldReceive('attempt')->with($data, true)->andReturn(true); + + $response = (new Login)->onLogin(); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/dashboard'); +}); + +it('logs in successfully with valid credentials and redirects to custom url', function() { + $data = ['email' => 'test@example.com', 'password' => 'password']; + request()->request->add($data); + request()->merge(['redirect' => 'orders']); + AdminAuth::shouldReceive('attempt')->with($data, true)->andReturn(true); + + $response = (new Login)->onLogin(); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/orders'); +}); + +it('fails to log in with invalid credentials', function() { + $data = ['email' => 'test@example.com', 'password' => 'wrong_password']; + request()->request->add($data); + AdminAuth::shouldReceive('attempt')->with($data, true)->andReturnFalse(); + + $this->expectException(ValidationException::class); + + (new Login)->onLogin(); +}); + +it('resets password successfully with valid code', function() { + User::factory()->create([ + 'reset_code' => 'valid_code', + 'reset_time' => now()->subMinutes(5), + ]); + $data = ['code' => 'valid_code', 'password' => 'new_password', 'password_confirm' => 'new_password']; + request()->request->add($data); + + $response = (new Login)->onResetPassword(); + + expect($response->getTargetUrl())->toBe('http://localhost/admin/login') + ->and(flash()->messages()->first())->level->toBe('success') + ->message->toBe(lang('igniter::admin.login.alert_success_reset')); +}); + +it('resets password fails if code does not match', function() { + User::factory()->create([ + 'reset_code' => 'valid_code', + 'reset_time' => now()->subMinutes(5), + ]); + $data = ['code' => 'invalid_code', 'password' => 'new_password', 'password_confirm' => 'new_password']; + request()->request->add($data); + + $this->expectException(ValidationException::class); + + (new Login)->onResetPassword(); +}); diff --git a/tests/Http/Controllers/LogoutTest.php b/tests/Http/Controllers/LogoutTest.php new file mode 100644 index 0000000..ebc82b9 --- /dev/null +++ b/tests/Http/Controllers/LogoutTest.php @@ -0,0 +1,19 @@ +post(route('igniter.admin.logout')) + ->assertRedirect(route('igniter.admin.login')); +}); + +it('logs out impersonator', function() { + $user = User::factory()->create(); + AdminAuth::getSession()->put(AdminAuth::getName().'_impersonate', $user->getKey()); + + $this->post(route('igniter.admin.logout')) + ->assertRedirect(route('igniter.admin.login')); +}); diff --git a/tests/Http/Controllers/NotificationsTest.php b/tests/Http/Controllers/NotificationsTest.php new file mode 100644 index 0000000..54507b9 --- /dev/null +++ b/tests/Http/Controllers/NotificationsTest.php @@ -0,0 +1,23 @@ +get(route('igniter.user.notifications')) + ->assertOk(); +}); + +it('marks notification as read', function() { + $user = User::factory()->superUser()->create(); + + $controller = new Notifications; + $controller->setUser($user); + $response = $controller->onMarkAsRead(); + + expect($response)->toBeInstanceOf(RedirectResponse::class); +}); diff --git a/tests/Http/Controllers/UserGroupsTest.php b/tests/Http/Controllers/UserGroupsTest.php new file mode 100644 index 0000000..dc8e4f3 --- /dev/null +++ b/tests/Http/Controllers/UserGroupsTest.php @@ -0,0 +1,94 @@ +get(route('igniter.user.user_groups')) + ->assertOk(); +}); + +it('loads create user group page', function() { + actingAsSuperUser() + ->get(route('igniter.user.user_groups', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads edit user group page', function() { + $userGroup = UserGroup::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.user_groups', ['slug' => 'edit/'.$userGroup->getKey()])) + ->assertOk(); +}); + +it('loads user group preview page', function() { + $userGroup = UserGroup::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.user_groups', ['slug' => 'preview/'.$userGroup->getKey()])) + ->assertOk(); +}); + +it('creates user group', function() { + actingAsSuperUser() + ->post(route('igniter.user.user_groups', ['slug' => 'create']), [ + 'UserGroup' => [ + 'user_group_name' => 'Created User Group', + 'auto_assign' => 0, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(UserGroup::where('user_group_name', 'Created User Group')->exists())->toBeTrue(); +}); + +it('updates user group', function() { + $userGroup = UserGroup::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.user_groups', ['slug' => 'edit/'.$userGroup->getKey()]), [ + 'UserGroup' => [ + 'user_group_name' => 'Updated User Group', + 'auto_assign' => 0, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(UserGroup::where('user_group_name', 'Updated User Group')->exists())->toBeTrue(); +}); + +it('deletes user group', function() { + $userGroup = UserGroup::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.user_groups', ['slug' => 'edit/'.$userGroup->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(UserGroup::find($userGroup->getKey()))->toBeNull(); +}); + +it('bulk deletes user groups', function() { + $userGroup = UserGroup::factory()->count(5)->create(); + $userGroupIds = $userGroup->pluck('user_group_id')->all(); + + actingAsSuperUser() + ->post(route('igniter.user.user_groups'), [ + 'checked' => $userGroupIds, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(UserGroup::whereIn('user_group_id', + $userGroupIds, + )->exists())->toBeFalse(); +}); diff --git a/tests/Http/Controllers/UserRolesTest.php b/tests/Http/Controllers/UserRolesTest.php new file mode 100644 index 0000000..f6b4516 --- /dev/null +++ b/tests/Http/Controllers/UserRolesTest.php @@ -0,0 +1,100 @@ +get(route('igniter.user.user_roles')) + ->assertOk(); +}); + +it('loads create user role page', function() { + actingAsSuperUser() + ->get(route('igniter.user.user_roles', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads edit user role page', function() { + $userRole = UserRole::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.user_roles', ['slug' => 'edit/'.$userRole->getKey()])) + ->assertOk(); +}); + +it('loads user role preview page', function() { + $userRole = UserRole::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.user_roles', ['slug' => 'preview/'.$userRole->getKey()])) + ->assertOk(); +}); + +it('creates user role', function() { + actingAsSuperUser() + ->post(route('igniter.user.user_roles', ['slug' => 'create']), [ + 'UserRole' => [ + 'name' => 'Created User Role', + 'permissions' => [ + 'Admin.Dashboard' => 1, + 'Admin.Users' => 1, + ], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(UserRole::where('name', 'Created User Role')->exists())->toBeTrue(); +}); + +it('updates user role', function() { + $userRole = UserRole::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.user_roles', ['slug' => 'edit/'.$userRole->getKey()]), [ + 'UserRole' => [ + 'name' => 'Updated User Role', + 'permissions' => [ + 'Admin.Dashboard' => 1, + 'Admin.Users' => 1, + ], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(UserRole::where('name', 'Updated User Role')->exists())->toBeTrue(); +}); + +it('deletes user role', function() { + $userRole = UserRole::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.user_roles', ['slug' => 'edit/'.$userRole->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(UserRole::find($userRole->getKey()))->toBeNull(); +}); + +it('bulk deletes user roles', function() { + $userRole = UserRole::factory()->count(5)->create(); + $userRoleIds = $userRole->pluck('user_role_id')->all(); + + actingAsSuperUser() + ->post(route('igniter.user.user_roles'), [ + 'checked' => $userRoleIds, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(UserRole::whereIn('user_role_id', + $userRoleIds, + )->exists())->toBeFalse(); +}); diff --git a/tests/Http/Controllers/UsersTest.php b/tests/Http/Controllers/UsersTest.php new file mode 100644 index 0000000..a0bc304 --- /dev/null +++ b/tests/Http/Controllers/UsersTest.php @@ -0,0 +1,198 @@ +get(route('igniter.user.users')) + ->assertOk(); +}); + +it('loads users page with no superadmin', function() { + $authGate = Mockery::mock(Gate::class); + $authGate->shouldReceive('inspect')->with('Admin.Staffs')->andReturnSelf(); + $authGate->shouldReceive('allowed')->andReturnTrue(); + app()->instance(Gate::class, $authGate); + $user = User::factory()->create(); + + actingAsSuperUser($user) + ->get(route('igniter.user.users')) + ->assertOk(); +}); + +it('loads create user page', function() { + actingAsSuperUser() + ->get(route('igniter.user.users', ['slug' => 'create'])) + ->assertOk(); +}); + +it('loads edit user page', function() { + $user = User::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.users', ['slug' => 'edit/'.$user->getKey()])) + ->assertOk(); +}); + +it('loads current user account page', function() { + $user = User::factory()->superUser()->create(); + + actingAsSuperUser($user) + ->get(route('igniter.user.users', ['slug' => 'account'])) + ->assertOk(); +}); + +it('loads user preview page', function() { + $user = User::factory()->create(); + + actingAsSuperUser() + ->get(route('igniter.user.users', ['slug' => 'preview/'.$user->getKey()])) + ->assertOk(); +}); + +it('creates user', function() { + actingAsSuperUser() + ->post(route('igniter.user.users', ['slug' => 'create']), [ + 'User' => [ + 'name' => 'John Doe', + 'username' => 'johndoe', + 'email' => 'user@example.com', + 'groups' => [1], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(User::where('email', 'user@example.com')->where('name', 'John Doe')->exists())->toBeTrue(); +}); + +it('updates user', function() { + $user = User::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.users', ['slug' => 'edit/'.$user->getKey()]), [ + 'User' => [ + 'name' => 'John Doe', + 'username' => 'johndoe', + 'email' => 'user@example.com', + 'groups' => [1], + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(User::where('email', 'user@example.com')->where('name', 'John Doe')->exists())->toBeTrue(); +}); + +it('updates current user and redirects user when email changes', function() { + $user = User::factory()->create(); + + actingAsSuperUser($user) + ->post(route('igniter.user.users', ['slug' => 'account']), [ + 'User' => [ + 'name' => 'John Doe', + 'username' => 'johndoe', + 'email' => 'user@example.com', + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(User::where('email', 'user@example.com')->where('name', 'John Doe')->exists())->toBeTrue(); +}); + +it('updates current user and redirects user when users changes', function() { + $user = User::factory()->superUser()->create(); + + actingAsSuperUser($user) + ->post(route('igniter.user.users', ['slug' => 'account']), [ + 'User' => [ + 'name' => 'John Doe', + 'username' => 'johndoe', + 'email' => $user->email, + ], + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onSave', + ]); + + expect(User::where('username', 'johndoe')->where('name', 'John Doe')->exists())->toBeTrue(); +}); + +it('deletes user', function() { + $user = User::factory()->create(); + + actingAsSuperUser() + ->post(route('igniter.user.users', ['slug' => 'edit/'.$user->getKey()]), [], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(User::find($user->getKey()))->toBeNull(); +}); + +it('bulk deletes users', function() { + $users = User::factory()->count(5)->create(); + $userIds = $users->pluck('user_id')->all(); + + actingAsSuperUser() + ->post(route('igniter.user.users'), [ + 'checked' => $userIds, + ], [ + 'X-Requested-With' => 'XMLHttpRequest', + 'X-IGNITER-REQUEST-HANDLER' => 'onDelete', + ]); + + expect(User::whereIn('user_id', $userIds)->exists())->toBeFalse(); +}); + +it('throws exception when unauthorized to delete user', function() { + $authGate = Mockery::mock(Gate::class); + $authGate->shouldReceive('inspect')->with('Admin.DeleteStaffs')->andReturnSelf(); + $authGate->shouldReceive('allowed')->andReturnFalse(); + app()->instance(Gate::class, $authGate); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(lang('igniter::admin.alert_user_restricted')); + + (new Users)->index_onDelete(); +}); + +it('impersonates user successfully', function() { + $authGate = Mockery::mock(Gate::class); + $authGate->shouldReceive('inspect')->with('Admin.Impersonate')->andReturnSelf(); + $authGate->shouldReceive('allowed')->andReturnTrue(); + app()->instance(Gate::class, $authGate); + $user = User::factory()->create(['name' => 'John Doe']); + request()->request->set('recordId', $user->getKey()); + + $controller = new Users; + $controller->setUser(User::factory()->superUser()->create()); + $controller->onImpersonate('edit'); + + expect(flash()->messages()->first()) + ->level->toBe('success') + ->message->toBe(sprintf(lang('igniter.user::default.staff.alert_impersonate_success'), 'John Doe')); +}); + +it('throws exception when unauthorized to impersonate user', function() { + $user = User::factory()->create(['name' => 'John Doe']); + $authGate = Mockery::mock(Gate::class); + $authGate->shouldReceive('inspect')->with('Admin.Impersonate')->andReturnSelf(); + $authGate->shouldReceive('allowed')->andReturnFalse(); + app()->instance(Gate::class, $authGate); + + $this->expectException(FlashException::class); + $this->expectExceptionMessage(lang('igniter.user::default.staff.alert_login_restricted')); + + (new Users)->onImpersonate('edit', $user->getKey()); +}); diff --git a/tests/Http/Middleware/InjectImpersonateBannerTest.php b/tests/Http/Middleware/InjectImpersonateBannerTest.php new file mode 100644 index 0000000..451eee0 --- /dev/null +++ b/tests/Http/Middleware/InjectImpersonateBannerTest.php @@ -0,0 +1,89 @@ +middleware = new InjectImpersonateBanner; + $this->response = Mockery::mock(Response::class); + $this->request = Mockery::mock(Request::class)->makePartial(); + app()->instance('request', $this->request); + + $this->next = function($request) { + return $this->response; + }; +}); + +it('injects banner into html response body when user is impersonator and route is theme', function() { + $this->request->shouldReceive('path')->andReturn('/'); + $this->request->shouldReceive('routeIs')->with('igniter.theme.*')->andReturnTrue(); + Auth::shouldReceive('check')->andReturnTrue(); + Auth::shouldReceive('isImpersonator')->andReturnTrue(); + $this->response->shouldReceive('getContent')->andReturn('Content'); + $this->response->shouldReceive('setContent')->with('Content
Impersonate Banner
'); + View::shouldReceive('make')->with('igniter.user::_partials.impersonate_banner')->andReturnSelf(); + View::shouldReceive('render')->andReturn('
Impersonate Banner
'); + + $result = $this->middleware->handle($this->request, $this->next); + + expect($result)->toBe($this->response); +}); + +it('append banner to response when user is impersonator and route is theme', function() { + $this->request->shouldReceive('path')->andReturn('/'); + $this->request->shouldReceive('routeIs')->with('igniter.theme.*')->andReturnTrue(); + Auth::shouldReceive('check')->andReturnTrue(); + Auth::shouldReceive('isImpersonator')->andReturnTrue(); + $this->response->shouldReceive('getContent')->andReturn('Content'); + $this->response->shouldReceive('setContent')->with('Content
Impersonate Banner
'); + View::shouldReceive('make')->with('igniter.user::_partials.impersonate_banner')->andReturnSelf(); + View::shouldReceive('render')->andReturn('
Impersonate Banner
'); + + $result = $this->middleware->handle($this->request, $this->next); + + expect($result)->toBe($this->response); +}); + +it('does not inject banner when route is not theme', function() { + $this->request->shouldReceive('routeIs')->with('igniter.theme.*')->andReturnFalse(); + + $result = $this->middleware->handle($this->request, $this->next); + + expect($result)->toBe($this->response); +}); + +it('does not inject banner when running in admin', function() { + $this->request->shouldReceive('routeIs')->with('igniter.theme.*')->andReturn(true); + $this->request->shouldReceive('path')->andReturn('admin/customers'); + + $result = $this->middleware->handle($this->request, $this->next); + + expect($result)->toBe($this->response); +}); + +it('does not inject banner when user is not logged in', function() { + $this->request->shouldReceive('routeIs')->with('igniter.theme.*')->andReturn(true); + $this->request->shouldReceive('path')->andReturn('/'); + Auth::shouldReceive('check')->andReturn(false); + + $result = $this->middleware->handle($this->request, $this->next); + + expect($result)->toBe($this->response); +}); + +it('does not inject banner when user is not impersonator', function() { + $this->request->shouldReceive('routeIs')->with('igniter.theme.*')->andReturn(true); + $this->request->shouldReceive('path')->andReturn('/'); + Auth::shouldReceive('check')->andReturn(true); + Auth::shouldReceive('isImpersonator')->andReturn(false); + + $result = $this->middleware->handle($this->request, $this->next); + + expect($result)->toBe($this->response); +}); diff --git a/tests/Http/Middleware/LogUserLastSeenTest.php b/tests/Http/Middleware/LogUserLastSeenTest.php new file mode 100644 index 0000000..d622e4d --- /dev/null +++ b/tests/Http/Middleware/LogUserLastSeenTest.php @@ -0,0 +1,43 @@ +middleware = new LogUserLastSeen; + $this->request = Mockery::mock(Request::class)->makePartial(); + app()->instance('request', $this->request); + $this->next = function($request) { + return 'next'; + }; +}); + +it('logs user last seen when database is available and user is authenticated', function() { + $authService = Mockery::mock(CustomerGuard::class); + $authService->shouldReceive('check')->andReturnTrue(); + $authService->shouldReceive('getId')->andReturn(1); + $authService->shouldReceive('user')->andReturn($customer = Mockery::mock(Customer::class)->makePartial()); + $customer->shouldReceive('updateLastSeen')->andReturnTrue()->atMost(2); + app()->instance('admin.auth', $authService); + app()->instance('main.auth', $authService); + + $response = $this->middleware->handle($this->request, $this->next); + + expect($response)->toBe('next'); +}); + +it('does not log user last seen when user is not authenticated', function() { + $authService = Mockery::mock(CustomerGuard::class); + $authService->shouldReceive('check')->andReturnFalse(); + app()->instance('admin.auth', $authService); + app()->instance('main.auth', $authService); + + $response = $this->middleware->handle($this->request, $this->next); + + expect($response)->toBe('next'); +}); diff --git a/tests/Http/Middleware/ThrottleRequestsTest.php b/tests/Http/Middleware/ThrottleRequestsTest.php new file mode 100644 index 0000000..86fbdab --- /dev/null +++ b/tests/Http/Middleware/ThrottleRequestsTest.php @@ -0,0 +1,57 @@ +rateLimiter = Mockery::mock(RateLimiter::class); + $this->rateLimiter->shouldReceive('tooManyAttempts')->andReturnTrue(); + $this->rateLimiter->shouldReceive('availableIn')->andReturn(1); + $this->middleware = new ThrottleRequests($this->rateLimiter); + $this->request = Mockery::mock(Request::class)->makePartial(); + $this->next = function($request) { + return 'next'; + }; +}); + +it('throttles request when shouldThrottleRequest returns true', function() { + $expectedParams = new \stdClass; + $expectedParams->maxAttempts = 6; + $expectedParams->decayMinutes = 1; + $expectedParams->prefix = ''; + $request = request(); + $request->setRouteResolver(fn() => new Route('GET', 'login', [])); + $request->headers->set('x-igniter-request-handler', 'index::onLogin'); + + Event::listen('igniter.user.beforeThrottleRequest', function($request, $params) use ($expectedParams) { + expect($params)->toEqual($expectedParams); + + return true; + }); + + expect(fn() => $this->middleware->handle($request, $this->next))->toThrow(ThrottleRequestsException::class); +}); + +it('does not throttle request when shouldThrottleRequest returns false', function() { + $expectedParams = new \stdClass; + $expectedParams->maxAttempts = 60; + $expectedParams->decayMinutes = 1; + $expectedParams->prefix = ''; + $request = request(); + $request->setRouteResolver(fn() => new Route('GET', 'login', [])); + + Event::listen('igniter.user.beforeThrottleRequest', function($request, $params) use ($expectedParams) { + expect($params)->toEqual($expectedParams); + + return false; + }); + + expect($this->middleware->handle($request, $this->next))->toBe('next'); +}); diff --git a/tests/Http/Requests/CustomerGroupRequestTest.php b/tests/Http/Requests/CustomerGroupRequestTest.php new file mode 100644 index 0000000..95c3268 --- /dev/null +++ b/tests/Http/Requests/CustomerGroupRequestTest.php @@ -0,0 +1,21 @@ +attributes(); + + expect($attributes['group_name'])->toBe(lang('igniter::admin.label_name')) + ->and($attributes['description'])->toBe(lang('igniter::admin.label_description')); +}); + +it('validates rules correctly for customer group', function() { + $rules = (new CustomerGroupRequest)->rules(); + + expect($rules['group_name'])->toContain('required', 'string', 'between:2,32') + ->and($rules['group_name'][3]->__toString())->toBe('unique:customer_groups,NULL,NULL,customer_group_id') + ->and($rules['approval'])->toBe(['required', 'boolean']) + ->and($rules['description'])->toBe(['string', 'between:2,512']); +}); diff --git a/tests/Http/Requests/CustomerRequestTest.php b/tests/Http/Requests/CustomerRequestTest.php new file mode 100644 index 0000000..5650d1a --- /dev/null +++ b/tests/Http/Requests/CustomerRequestTest.php @@ -0,0 +1,44 @@ +attributes(); + + expect($attributes['first_name'])->toBe(lang('igniter.user::default.customers.label_first_name')) + ->and($attributes['last_name'])->toBe(lang('igniter.user::default.customers.label_last_name')) + ->and($attributes['email'])->toBe(lang('igniter::admin.label_email')) + ->and($attributes['telephone'])->toBe(lang('igniter.user::default.customers.label_telephone')) + ->and($attributes['newsletter'])->toBe(lang('igniter.user::default.customers.label_newsletter')) + ->and($attributes['customer_group_id'])->toBe(lang('igniter.user::default.customers.label_customer_group')) + ->and($attributes['status'])->toBe(lang('igniter::admin.label_status')) + ->and($attributes['addresses.*.address_1'])->toBe(lang('igniter.user::default.customers.label_address_1')) + ->and($attributes['addresses.*.city'])->toBe(lang('igniter.user::default.customers.label_city')) + ->and($attributes['addresses.*.state'])->toBe(lang('igniter.user::default.customers.label_state')) + ->and($attributes['addresses.*.postcode'])->toBe(lang('igniter.user::default.customers.label_postcode')) + ->and($attributes['addresses.*.country_id'])->toBe(lang('igniter.user::default.customers.label_country')) + ->and($attributes['password'])->toBe(lang('igniter.user::default.customers.label_password')) + ->and($attributes['confirm_password'])->toBe(lang('igniter.user::default.customers.label_confirm_password')); +}); + +it('validates rules correctly for customer', function() { + $rules = (new CustomerRequest)->rules(); + + expect($rules['first_name'])->toBe(['required', 'string', 'between:1,48']) + ->and($rules['last_name'])->toBe(['required', 'string', 'between:1,48']) + ->and($rules['email'])->toContain('required', 'email:filter', 'max:96') + ->and($rules['email'][3]->__toString())->toBe('unique:customers,NULL,NULL,customer_id') + ->and($rules['password'])->toBe(['nullable', 'required_if:send_invite,0', 'string', 'min:8', 'max:40', 'same:confirm_password']) + ->and($rules['telephone'])->toBe(['nullable', 'string']) + ->and($rules['newsletter'])->toBe(['nullable', 'required', 'boolean']) + ->and($rules['customer_group_id'])->toBe(['required', 'integer']) + ->and($rules['status'])->toBe(['required', 'boolean']) + ->and($rules['addresses.*.address_id'])->toBe(['nullable', 'integer']) + ->and($rules['addresses.*.address_1'])->toBe(['required', 'string', 'min:3', 'max:255']) + ->and($rules['addresses.*.address_2'])->toBe(['nullable', 'string']) + ->and($rules['addresses.*.city'])->toBe(['nullable', 'string', 'min:2', 'max:255']) + ->and($rules['addresses.*.state'])->toBe(['nullable', 'string', 'max:255']) + ->and($rules['addresses.*.postcode'])->toBe(['nullable', 'string']); +}); diff --git a/tests/Http/Requests/UserGroupRequestTest.php b/tests/Http/Requests/UserGroupRequestTest.php new file mode 100644 index 0000000..542e7f0 --- /dev/null +++ b/tests/Http/Requests/UserGroupRequestTest.php @@ -0,0 +1,28 @@ +attributes(); + + expect($attributes['user_group_name'])->toBe(lang('igniter::admin.label_name')) + ->and($attributes['description'])->toBe(lang('igniter::admin.label_description')) + ->and($attributes['auto_assign'])->toBe(lang('igniter.user::default.user_groups.label_auto_assign')) + ->and($attributes['auto_assign_mode'])->toBe(lang('igniter.user::default.user_groups.label_assignment_mode')) + ->and($attributes['auto_assign_limit'])->toBe(lang('igniter.user::default.user_groups.label_load_balanced_limit')) + ->and($attributes['auto_assign_availability'])->toBe(lang('igniter.user::default.user_groups.label_assignment_availability')); +}); + +it('validates rules correctly for user group', function() { + $rules = (new UserGroupRequest)->rules(); + + expect($rules['user_group_name'])->toContain('required', 'string', 'between:2,255') + ->and($rules['user_group_name'][3]->__toString())->toBe('unique:admin_user_groups,NULL,NULL,user_group_id') + ->and($rules['description'])->toBe(['string']) + ->and($rules['auto_assign'])->toBe(['required', 'boolean']) + ->and($rules['auto_assign_mode'])->toBe(['required_if:auto_assign,true', 'integer', 'max:2']) + ->and($rules['auto_assign_limit'])->toBe(['required_if:auto_assign_mode,2', 'integer', 'max:99']) + ->and($rules['auto_assign_availability'])->toBe(['required_if:auto_assign,true', 'boolean']); +}); diff --git a/tests/Http/Requests/UserRequestTest.php b/tests/Http/Requests/UserRequestTest.php new file mode 100644 index 0000000..d8c9120 --- /dev/null +++ b/tests/Http/Requests/UserRequestTest.php @@ -0,0 +1,41 @@ +attributes(); + + expect($attributes['name'])->toBe(lang('igniter::admin.label_name')) + ->and($attributes['email'])->toBe(lang('igniter::admin.label_email')) + ->and($attributes['username'])->toBe(lang('igniter.user::default.staff.label_username')) + ->and($attributes['password'])->toBe(lang('igniter.user::default.staff.label_password')) + ->and($attributes['password_confirm'])->toBe(lang('igniter.user::default.staff.label_confirm_password')) + ->and($attributes['status'])->toBe(lang('igniter::admin.label_status')) + ->and($attributes['language_id'])->toBe(lang('igniter.user::default.staff.label_language_id')) + ->and($attributes['user_role_id'])->toBe(lang('igniter.user::default.staff.label_role')) + ->and($attributes['groups'])->toBe(lang('igniter.user::default.staff.label_group')) + ->and($attributes['locations'])->toBe(lang('igniter.user::default.staff.label_location')) + ->and($attributes['groups.*'])->toBe(lang('igniter.user::default.staff.label_group')) + ->and($attributes['locations.*'])->toBe(lang('igniter.user::default.staff.label_location')); +}); + +it('has correct validation rules', function() { + $rules = (new UserRequest)->rules(); + + expect($rules['name'])->toBe(['required', 'string', 'between:2,255']) + ->and($rules['email'])->toContain('required', 'email:filter', 'max:96') + ->and($rules['email'][3]->__toString())->toBe('unique:admin_users,NULL,NULL,user_id') + ->and($rules['username'])->toContain('required', 'alpha_dash', 'between:2,32') + ->and($rules['username'][3]->__toString())->toBe('unique:admin_users,NULL,NULL,user_id') + ->and($rules['password'])->toBe(['nullable', 'required_if:send_invite,0', 'string', 'between:6,32', 'same:password_confirm']) + ->and($rules['status'])->toBe(['boolean']) + ->and($rules['super_user'])->toBe(['boolean']) + ->and($rules['language_id'])->toBe(['nullable', 'integer']) + ->and($rules['user_role_id'])->toBe(['sometimes', 'required', 'integer']) + ->and($rules['groups'])->toBe(['sometimes', 'required', 'array']) + ->and($rules['locations'])->toBe(['nullable', 'array']) + ->and($rules['groups.*'])->toBe(['integer']) + ->and($rules['locations.*'])->toBe(['integer']); +}); diff --git a/tests/Http/Requests/UserRoleRequestTest.php b/tests/Http/Requests/UserRoleRequestTest.php new file mode 100644 index 0000000..46ef510 --- /dev/null +++ b/tests/Http/Requests/UserRoleRequestTest.php @@ -0,0 +1,24 @@ +attributes(); + + expect($attributes['code'])->toBe(lang('igniter::admin.label_code')) + ->and($attributes['name'])->toBe(lang('igniter::admin.label_name')) + ->and($attributes['permissions'])->toBe(lang('igniter.user::default.user_roles.label_permissions')) + ->and($attributes['permissions.*'])->toBe(lang('igniter.user::default.user_roles.label_permissions')); +}); + +it('validates rules correctly for user role', function() { + $rules = (new UserRoleRequest)->rules(); + + expect($rules['code'])->toBe(['string', 'between:2,32', 'alpha_dash']) + ->and($rules['name'])->toContain('required', 'string', 'between:2,255') + ->and($rules['name'][3]->__toString())->toBe('unique:admin_user_roles,NULL,NULL,user_role_id') + ->and($rules['permissions'])->toBe(['required', 'array']) + ->and($rules['permissions.*'])->toBe(['required', 'integer']); +}); diff --git a/tests/Http/Requests/UserSettingsRequestTest.php b/tests/Http/Requests/UserSettingsRequestTest.php new file mode 100644 index 0000000..5c8292a --- /dev/null +++ b/tests/Http/Requests/UserSettingsRequestTest.php @@ -0,0 +1,20 @@ +attributes(); + + expect($attributes['allow_registration'])->toBe(lang('igniter::system.settings.label_allow_registration')) + ->and($attributes['registration_email.*'])->toBe(lang('igniter::system.settings.label_registration_email')); +}); + +it('validates rules correctly for user settings', function() { + $rules = (new UserSettingsRequest)->rules(); + + expect($rules['allow_registration'])->toBe(['required', 'integer']) + ->and($rules['registration_email'])->toBe(['required', 'array']) + ->and($rules['registration_email.*'])->toBe(['required', 'alpha']); +}); diff --git a/tests/Jobs/AllocateAssignableTest.php b/tests/Jobs/AllocateAssignableTest.php new file mode 100644 index 0000000..aeff141 --- /dev/null +++ b/tests/Jobs/AllocateAssignableTest.php @@ -0,0 +1,83 @@ +makePartial(); + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $assignee = Mockery::mock(User::class)->makePartial(); + $assignable = Mockery::mock(Order::class)->makePartial(); + $assignableLog->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + $assignableLog->shouldReceive('extendableGet')->with('assignee_id')->andReturn(null); + $assignableLog->shouldReceive('extendableGet')->with('assignee_group')->andReturn($userGroup); + $userGroup->shouldReceive('findAvailableAssignee')->andReturn($assignee); + $assignableLog->assignable->shouldReceive('assignTo')->with($assignee)->once(); + + (new AllocateAssignable($assignableLog))->handle(); +}); + +it('does not allocate when assignee_id is already set', function() { + $assignableLog = Mockery::mock(AssignableLog::class)->makePartial(); + $assignableLog->shouldReceive('extendableGet')->with('assignee_id')->andReturn(1); + $assignableLog->shouldReceive('extendableGet')->with('assignee_group')->never(); + + (new AllocateAssignable($assignableLog))->handle(); +}); + +it('does not allocate when assignable does not use Assignable trait', function() { + $assignableLog = Mockery::mock(AssignableLog::class)->makePartial(); + $assignableLog->shouldReceive('extendableGet')->with('assignee_id')->andReturnNull(); + $assignableLog->shouldReceive('extendableGet')->with('assignable')->andReturn(new \stdClass); + $assignableLog->shouldReceive('extendableGet')->with('assignee_group')->never(); + + (new AllocateAssignable($assignableLog))->handle(); +}); + +it('does not allocate when assignee_group is not an instance of UserGroup', function() { + $assignableLog = Mockery::mock(AssignableLog::class)->makePartial(); + $assignable = Mockery::mock(Order::class)->makePartial(); + $assignableLog->shouldReceive('extendableGet')->with('assignee_id')->andReturnNull(); + $assignableLog->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + $assignableLog->shouldReceive('extendableGet')->with('assignee_group')->andReturnNull()->once(); + + (new AllocateAssignable($assignableLog))->handle(); +}); + +it('retries allocation when no assignee is available and not last attempt', function() { + $assignableLog = Mockery::mock(AssignableLog::class)->makePartial(); + $assignable = Mockery::mock(Order::class)->makePartial(); + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $assignableLog->shouldReceive('extendableGet')->with('assignee_id')->andReturnNull(); + $assignableLog->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + $assignableLog->shouldReceive('extendableGet')->with('assignee_group')->andReturn($userGroup); + $userGroup->shouldReceive('findAvailableAssignee')->andReturn(null); + + $job = Mockery::mock(AllocateAssignable::class, [$assignableLog])->makePartial(); + $job->shouldReceive('attempts')->andReturn(1); + $job->shouldReceive('release')->with(10)->once(); + + $job->handle(); +}); + +it('deletes job when no assignee is available and last attempt', function() { + $assignableLog = Mockery::mock(AssignableLog::class)->makePartial(); + $assignable = Mockery::mock(Order::class)->makePartial(); + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $assignableLog->shouldReceive('extendableGet')->with('assignee_id')->andReturnNull(); + $assignableLog->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + $assignableLog->shouldReceive('extendableGet')->with('assignee_group')->andReturn($userGroup); + $userGroup->shouldReceive('findAvailableAssignee')->andReturn(null); + + $job = Mockery::mock(AllocateAssignable::class, [$assignableLog])->makePartial(); + $job->shouldReceive('attempts')->andReturn(3); + $job->shouldReceive('delete')->once(); + + $job->handle(); +}); diff --git a/tests/MainMenuWidgets/NotificationListTest.php b/tests/MainMenuWidgets/NotificationListTest.php new file mode 100644 index 0000000..a9d6747 --- /dev/null +++ b/tests/MainMenuWidgets/NotificationListTest.php @@ -0,0 +1,52 @@ +user = Mockery::mock(User::class)->makePartial(); + $this->mainMenuItem = new MainMenuItem('testField', 'Label'); + $controller = resolve(Menus::class); + $controller->setUser($this->user); + $this->notificationList = new NotificationList($controller, $this->mainMenuItem, [ + 'model' => Mockery::mock(Model::class)->makePartial(), + ]); +}); + +it('renders notification list with unread count', function() { + $this->user->shouldReceive('unreadNotifications')->andReturnSelf(); + $this->user->shouldReceive('count')->andReturn(5); + + expect($this->notificationList->render())->toBeString() + ->and($this->notificationList->vars['unreadCount'])->toBe(5); +}); + +it('returns dropdown options with notifications', function() { + $this->user->shouldReceive('notifications')->andReturnSelf(); + $this->user->shouldReceive('get')->andReturn(collect([new Notification])); + + $result = $this->notificationList->onDropdownOptions(); + + expect($result)->toHaveKey('#'.$this->notificationList->getId('options')) + ->and($result['#'.$this->notificationList->getId('options')])->not->toBeEmpty() + ->and($this->notificationList->vars['notifications'])->toBeCollection() + ->and($this->notificationList->vars['notifications'])->toHaveCount(1) + ->and($this->notificationList->vars['notifications']->first())->toBeInstanceOf(Notification::class); +}); + +it('marks notifications as read and returns updated list', function() { + $this->user->shouldReceive('unreadNotifications')->andReturnSelf(); + $this->user->shouldReceive('update')->with(['read_at' => now()])->andReturn(1); + + $result = $this->notificationList->onMarkAsRead(); + + expect($result)->toHaveKey('~#'.$this->notificationList->getId()) + ->and($result['~#'.$this->notificationList->getId()])->not->toBeEmpty(); +}); diff --git a/tests/MainMenuWidgets/UserPanelTest.php b/tests/MainMenuWidgets/UserPanelTest.php new file mode 100644 index 0000000..4a8c207 --- /dev/null +++ b/tests/MainMenuWidgets/UserPanelTest.php @@ -0,0 +1,84 @@ +user = Mockery::mock(User::class)->makePartial(); + $this->mainMenuItem = new MainMenuItem('testField', 'Label'); + $controller = resolve(Menus::class); + $controller->setUser($this->user); + $this->userPanel = new UserPanel($controller, $this->mainMenuItem, [ + 'model' => Mockery::mock(Model::class)->makePartial(), + ]); +}); + +it('renders user panel with correct variables', function() { + $this->userPanel->links = [ + 'test' => [ + 'label' => 'Test', + 'url' => 'test', + ], + ]; + $result = $this->userPanel->render(); + + expect($result)->toBeString() + ->and($this->userPanel->vars['avatarUrl'])->toBe('//www.gravatar.com/avatar/d41d8cd98f00b204e9800998ecf8427e.png?d=mm') + ->and($this->userPanel->vars['userName'])->toBeNull() + ->and($this->userPanel->vars['roleName'])->toBeNull() + ->and($this->userPanel->vars['userIsOnline'])->toBeTrue() + ->and($this->userPanel->vars['userIsIdle'])->toBeFalse() + ->and($this->userPanel->vars['userIsAway'])->toBeFalse() + ->and($this->userPanel->vars['userStatusName'])->toBe('igniter.user::default.staff_status.text_online') + ->and($this->userPanel->vars['links'])->toBeCollection(); +}); + +it('loads status form with correct variables', function() { + $result = $this->userPanel->onLoadStatusForm(); + + expect($result)->toBeString() + ->and($this->userPanel->vars['statuses'])->toBe([ + UserState::ONLINE_STATUS => 'igniter.user::default.staff_status.text_online', + UserState::BACK_SOON_STATUS => 'igniter.user::default.staff_status.text_back_soon', + UserState::AWAY_STATUS => 'igniter.user::default.staff_status.text_away', + UserState::CUSTOM_STATUS => 'igniter.user::default.staff_status.text_custom_status', + ]) + ->and($this->userPanel->vars['clearAfterOptions'])->toHaveKeys([1440, 240, 30, 0]) + ->and($this->userPanel->vars['message'])->toBeNull() + ->and($this->userPanel->vars['userStatus'])->toBe(1) + ->and($this->userPanel->vars['clearAfterMinutes'])->toBe(0) + ->and($this->userPanel->vars['statusUpdatedAt'])->toBeNull(); +}); + +it('sets status successfully with valid data', function() { + request()->request->add([ + 'status' => 1, + 'message' => 'Available', + 'clear_after' => 30, + ]); + + $this->user->shouldReceive('extendableGet')->with('user_id')->andReturn(1); + $result = $this->userPanel->onSetStatus(); + + expect($result)->toBe(['~#'.$this->userPanel->getId() => $this->userPanel->render()]); +}); + +it('throws exception when setting invalid status', function() { + request()->request->add([ + 'status' => 0, + 'message' => '', + 'clear_after' => 30, + ]); + + $this->expectException(FlashException::class); + + $this->userPanel->onSetStatus(); +}); diff --git a/tests/Models/AddressTest.php b/tests/Models/AddressTest.php new file mode 100644 index 0000000..6af2921 --- /dev/null +++ b/tests/Models/AddressTest.php @@ -0,0 +1,71 @@ + 1, + 'address_id' => 1, + 'address_1' => '123 Main St', + 'address_2' => 'Apt 4', + 'city' => 'Anytown', + 'state' => 'CA', + 'postcode' => '12345', + 'country_id' => 1, + ]; + + Address::createOrUpdateFromRequest($addressData); + + expect(Address::where($addressData)->exists())->toBeTrue(); +}); + +it('returns formatted address attribute', function() { + $expectedAddress = '123 Main St, Apt 4, Anytown 12345, CA, Afghanistan'; + $address = Mockery::mock(Address::class)->makePartial(); + $address->shouldReceive('toArray')->andReturn([ + 'address_1' => '123 Main St', + 'address_2' => 'Apt 4', + 'city' => 'Anytown', + 'state' => 'CA', + 'postcode' => '12345', + 'country_id' => 1, + ]); + $address->shouldReceive('format_address')->with(Mockery::type('array'), false)->andReturn($expectedAddress); + + $result = $address->getFormattedAddressAttribute(null); + + expect($result)->toBe($expectedAddress); +}); + +it('applies filters to query builder', function() { + $query = Address::query()->applyFilters([ + 'customer' => 1, + 'sort' => 'address_id desc', + ]); + + expect($query->toSql())->toContain('where `addresses`.`customer_id` = ?') + ->and($query->toSql())->toContain('order by `address_id` desc'); +}); + +it('configures address model correctly', function() { + $address = new Address; + + expect(class_uses_recursive($address)) + ->toContain(HasCountry::class) + ->toContain(HasCustomer::class) + ->and($address->getTable())->toBe('addresses') + ->and($address->getKeyName())->toBe('address_id') + ->and($address->getFillable())->toBe(['customer_id', 'address_1', 'address_2', 'city', 'state', 'postcode', 'country_id']) + ->and($address->getCasts()['customer_id'])->toBe('integer') + ->and($address->getCasts()['country_id'])->toBe('integer') + ->and($address->getMorphClass())->toBe('addresses') + ->and($address->relation['belongsTo']['customer'])->toBe(Customer::class) + ->and($address->relation['belongsTo']['country'])->toBe(Country::class); +}); diff --git a/tests/Models/AssignableLogTest.php b/tests/Models/AssignableLogTest.php new file mode 100644 index 0000000..d07f8fa --- /dev/null +++ b/tests/Models/AssignableLogTest.php @@ -0,0 +1,180 @@ +makePartial(); + $assignable->shouldReceive('getMorphClass')->andReturn('order'); + $assignable->shouldReceive('getKey')->andReturn(1); + $assignable->assignee_group_id = 1; + $assignable->assignee_id = 2; + $assignable->status_id = 3; + $assignable->shouldReceive('newQuery->where->update')->with(Mockery::on(function($data) { + return isset($data['assignee_updated_at']); + }))->once(); + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getKey')->andReturn(1); + + $result = AssignableLog::createLog($assignable, $user); + + expect($result)->toBeInstanceOf(AssignableLog::class) + ->and($result->user_id)->toBe(1) + ->and($result->status_id)->toBe(3); +}); + +it('creates log without user and updates assignee_updated_at', function() { + $assignable = Mockery::mock(Order::class)->makePartial(); + $assignable->shouldReceive('getMorphClass')->andReturn('order'); + $assignable->shouldReceive('getKey')->andReturn(1); + $assignable->assignee_group_id = 1; + $assignable->assignee_id = 2; + $assignable->status_id = 3; + $assignable->shouldReceive('newQuery->where->update')->with(Mockery::on(function($data) { + return isset($data['assignee_updated_at']); + }))->once(); + + $result = AssignableLog::createLog($assignable); + + expect($result)->toBeInstanceOf(AssignableLog::class) + ->and($result->user_id)->toBeNull() + ->and($result->status_id)->toBe(3); +}); + +it('returns true when assignable type is order', function() { + $log = Mockery::mock(AssignableLog::class)->makePartial(); + $log->assignable_type = \Igniter\Cart\Models\Order::make()->getMorphClass(); + + $result = $log->isForOrder(); + + expect($result)->toBeTrue(); +}); + +it('returns false when assignable type is not order', function() { + $log = Mockery::mock(AssignableLog::class)->makePartial(); + $log->assignable_type = 'some_other_type'; + + $result = $log->isForOrder(); + + expect($result)->toBeFalse(); +}); + +it('applies assignable scope with correct type and id', function() { + $query = Mockery::mock(QueryBuilder::class); + $assignable = Mockery::mock(Model::class); + $assignable->shouldReceive('getMorphClass')->andReturn('order'); + $assignable->shouldReceive('getKey')->andReturn(1); + + $query->shouldReceive('where')->with('assignable_type', 'order')->andReturnSelf(); + $query->shouldReceive('where')->with('assignable_id', 1)->andReturnSelf(); + + $log = new AssignableLog; + $result = $log->scopeApplyAssignable($query, $assignable); + + expect($result)->toBe($query); +}); + +it('applies round robin scope with correct order', function() { + $query = Mockery::mock(QueryBuilder::class); + $query->shouldReceive('select')->with('assignee_id')->andReturnSelf(); + $query->shouldReceive('selectRaw')->with('MAX(created_at) as assign_value')->andReturnSelf()->once(); + $query->shouldReceive('whereIn')->with('status_id', Mockery::type('array'))->andReturnSelf(); + $query->shouldReceive('whereNotNull')->with('assignee_id')->andReturnSelf(); + $query->shouldReceive('groupBy')->with('assignee_id')->andReturnSelf(); + $query->shouldReceive('orderBy')->with('assign_value', 'asc')->andReturnSelf(); + + $log = new AssignableLog; + $result = $log->scopeApplyRoundRobinScope($query); + + expect($result)->toBe($query); +}); + +it('applies load balanced scope with correct limit', function() { + $query = Mockery::mock(QueryBuilder::class); + $limit = 10; + $query->shouldReceive('select')->with('assignee_id')->andReturnSelf(); + $query->shouldReceive('selectRaw')->with('COUNT(assignee_id)/'.DB::getPdo()->quote($limit).' as assign_value')->andReturnSelf()->once(); + $query->shouldReceive('whereIn')->with('status_id', Mockery::type('array'))->andReturnSelf(); + $query->shouldReceive('whereNotNull')->with('assignee_id')->andReturnSelf(); + $query->shouldReceive('groupBy')->with('assignee_id')->andReturnSelf(); + $query->shouldReceive('orderBy')->with('assign_value', 'desc')->andReturnSelf(); + $query->shouldReceive('havingRaw')->with('assign_value < 1')->andReturnSelf()->once(); + + $log = new AssignableLog; + $result = $log->scopeApplyLoadBalancedScope($query, $limit); + + expect($result)->toBe($query); +}); + +it('applies scope with correct assignee id', function() { + $query = Mockery::mock(QueryBuilder::class); + $assigneeId = 1; + $query->shouldReceive('where')->with('assignee_id', $assigneeId)->andReturnSelf()->once(); + + $log = new AssignableLog; + $result = $log->scopeWhereAssignTo($query, $assigneeId); + + expect($result)->toBe($query); +}); + +it('applies scope with correct assignee group id', function() { + $query = Mockery::mock(QueryBuilder::class); + $assigneeGroupId = 1; + $query->shouldReceive('where')->with('assignee_group_id', $assigneeGroupId)->andReturnSelf()->once(); + + $log = new AssignableLog; + $result = $log->scopeWhereAssignToGroup($query, $assigneeGroupId); + + expect($result)->toBe($query); +}); + +it('applies scope with correct assignee group ids', function() { + $query = Mockery::mock(QueryBuilder::class); + $assigneeGroupIds = [1, 2, 3]; + $query->shouldReceive('whereIn')->with('assignee_group_id', $assigneeGroupIds)->andReturnSelf()->once(); + + $log = new AssignableLog; + $result = $log->scopeWhereInAssignToGroup($query, $assigneeGroupIds); + + expect($result)->toBe($query); +}); + +it('returns prunable records older than activity log timeout', function() { + $this->travelTo($date = now()->startOfDay()); + + $result = (new AssignableLog)->prunable()->toRawSql(); + expect($result)->toContain("`created_at` <= '".$date->subDays(60)->toDateTimeString()."'"); +}); + +it('configures assignable log model correctly', function() { + $log = new AssignableLog; + + expect(class_uses_recursive($log)) + ->toContain(Prunable::class) + ->and($log->getTable())->toBe('assignable_logs') + ->and($log->getKeyName())->toBe('id') + ->and($log->getGuarded())->toBe([]) + ->and($log->timestamps)->toBeTrue() + ->and($log->getCasts()['assignable_id'])->toBe('integer') + ->and($log->getCasts()['assignee_group_id'])->toBe('integer') + ->and($log->getCasts()['assignee_id'])->toBe('integer') + ->and($log->getCasts()['user_id'])->toBe('integer') + ->and($log->getCasts()['status_id'])->toBe('integer') + ->and($log->getMorphClass())->toBe('assignable_logs') + ->and($log->relation['belongsTo']['user'])->toBe(User::class) + ->and($log->relation['belongsTo']['assignee'])->toBe(User::class) + ->and($log->relation['belongsTo']['assignee_group'])->toBe(UserGroup::class) + ->and($log->relation['belongsTo']['status'])->toBe(Status::class) + ->and($log->relation['morphTo']['assignable'])->toBe([]); +}); diff --git a/tests/Models/Concerns/AssignableTest.php b/tests/Models/Concerns/AssignableTest.php new file mode 100644 index 0000000..108553a --- /dev/null +++ b/tests/Models/Concerns/AssignableTest.php @@ -0,0 +1,288 @@ +makePartial(); + $assignee = Mockery::mock(User::class)->makePartial(); + $user = Mockery::mock(User::class)->makePartial(); + $group = Mockery::mock(UserGroup::class)->makePartial(); + $assignable->assignee = null; + $assignable->assignee_group = $group; + $assignable->assignee_group_id = 1; + $assignable->assignee_id = 1; + $assignable->status_id = 1; + $assignee->shouldReceive('groups->first')->andReturnNull(); + $assignable->shouldReceive('assignee_group->associate')->with($group)->once(); + $assignable->shouldReceive('assignee->associate')->with($assignee)->once(); + $assignable->shouldReceive('fireSystemEvent')->with('admin.assignable.beforeAssignTo', [$group, $assignee, null, $group])->once(); + $assignable->shouldReceive('save')->andReturnTrue(); + $assignable->shouldReceive('fireSystemEvent')->with('admin.assignable.assigned', Mockery::any())->once(); + $assignable->shouldReceive('getMorphClass')->andReturn('assignable'); + $assignable->shouldReceive('getKey')->andReturn(1); + $assignable->shouldReceive('getKeyName')->andReturn('id'); + $assignable->shouldReceive('newQuery->where->update')->once(); + + $result = $assignable->assignTo($assignee, $user); + + expect($result)->toBeInstanceOf(AssignableLog::class); +}); + +it('updates assign to successfully', function() { + Event::fake(); + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $assignee = Mockery::mock(User::class)->makePartial(); + $user = Mockery::mock(User::class)->makePartial(); + $assignable->assignee = null; + $assignable->assignee_group = null; + $assignable->assignee_group_id = 1; + $assignable->assignee_id = 1; + $assignable->status_id = 1; + $assignee->shouldReceive('groups->first')->andReturnNull(); + $assignable->shouldReceive('assignee_group->dissociate')->once(); + $assignable->shouldReceive('assignee->associate')->with($assignee)->once(); + $assignable->shouldReceive('fireSystemEvent')->with('admin.assignable.beforeAssignTo', [null, $assignee, null, null])->once(); + $assignable->shouldReceive('save')->andReturnTrue(); + $assignable->shouldReceive('fireSystemEvent')->with('admin.assignable.assigned', Mockery::any())->once(); + $assignable->shouldReceive('getMorphClass')->andReturn('assignable'); + $assignable->shouldReceive('getKey')->andReturn(1); + $assignable->shouldReceive('getKeyName')->andReturn('id'); + $assignable->shouldReceive('newQuery->where->update')->once(); + + $result = $assignable->updateAssignTo(null, $assignee, $user); + + expect($result)->toBeInstanceOf(AssignableLog::class); +}); + +it('throws exception when assignee group is not set', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $assignee = Mockery::mock(User::class); + + $assignable->assignee_group = null; + + $this->expectException(FlashException::class); + $this->expectExceptionMessage('Assignee group is not set'); + + $assignable->assignTo($assignee); +}); + +it('assigns to group successfully', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $group = Mockery::mock(UserGroup::class)->makePartial(); + $user = Mockery::mock(User::class)->makePartial(); + $assignable->assignee = null; + $assignable->assignee_group = null; + $assignable->assignee_group_id = 1; + $assignable->assignee_id = 1; + $assignable->status_id = 1; + $assignable->shouldReceive('assignee_group->associate')->with($group)->once(); + $assignable->shouldReceive('assignee->dissociate')->once(); + $assignable->shouldReceive('fireSystemEvent')->with('admin.assignable.beforeAssignTo', [$group, null, null, null])->once(); + $assignable->shouldReceive('save')->andReturnTrue(); + $assignable->shouldReceive('fireSystemEvent')->with('admin.assignable.assigned', Mockery::any())->once(); + $assignable->shouldReceive('getMorphClass')->andReturn('assignable'); + $assignable->shouldReceive('getKey')->andReturn(1); + $assignable->shouldReceive('getKeyName')->andReturn('id'); + $assignable->shouldReceive('newQuery->where->update')->once(); + + $result = $assignable->assignToGroup($group, $user); + + expect($result)->toBeInstanceOf(AssignableLog::class); +}); + +it('returns true when assigned to user', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $assignable->assignee = Mockery::mock(User::class); + + $result = $assignable->hasAssignTo(); + + expect($result)->toBeTrue(); +}); + +it('returns false when not assigned to user', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $assignable->assignee = null; + + $result = $assignable->hasAssignTo(); + + expect($result)->toBeFalse(); +}); + +it('returns true when assigned to group', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $assignable->assignee_group = Mockery::mock(UserGroup::class); + + $result = $assignable->hasAssignToGroup(); + + expect($result)->toBeTrue(); +}); + +it('returns false when not assigned to group', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $assignable->assignee_group = null; + + $result = $assignable->hasAssignToGroup(); + + expect($result)->toBeFalse(); +}); + +it('lists group assignees when group is set', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $group = Mockery::mock(UserGroup::class); + $assignable->assignee_group = $group; + + $group->shouldReceive('listAssignees')->andReturn(['assignee1', 'assignee2']); + + $result = $assignable->listGroupAssignees(); + + expect($result)->toBe(['assignee1', 'assignee2']); +}); + +it('returns empty list when group is not set', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $assignable->assignee_group = null; + + $result = $assignable->listGroupAssignees(); + + expect($result)->toBe([]); +}); + +it('returns true when user cannot be assigned to staff', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $user = Mockery::mock(User::class)->makePartial(); + $assignableLog = Mockery::mock(AssignableLog::class)->makePartial(); + $assignable->assignee_group_id = 1; + $user->shouldReceive('getKey')->andReturn(1); + $assignable->shouldReceive('assignable_logs')->andReturn($assignableLog); + $assignableLog->shouldReceive('where')->with('user_id', 1)->andReturnSelf(); + $assignableLog->shouldReceive('where')->with('assignee_group_id', $assignable->assignee_group_id)->andReturnSelf(); + $assignableLog->shouldReceive('exists')->andReturn(true); + + $result = $assignable->cannotAssignToStaff($user); + + expect($result)->toBeTrue(); +}); + +it('returns false when user can be assigned to staff', function() { + $assignable = Mockery::mock(Assignable::class)->makePartial(); + $user = Mockery::mock(User::class)->makePartial(); + $assignable->assignee_group_id = 1; + $assignableLog = Mockery::mock(AssignableLog::class)->makePartial(); + + $user->shouldReceive('getKey')->andReturn(1); + $assignable->shouldReceive('assignable_logs')->andReturn($assignableLog); + $assignableLog->shouldReceive('where')->with('user_id', 1)->andReturnSelf(); + $assignableLog->shouldReceive('where')->with('assignee_group_id', $assignable->assignee_group_id)->andReturnSelf(); + $assignableLog->shouldReceive('exists')->andReturn(false); + + $result = $assignable->cannotAssignToStaff($user); + + expect($result)->toBeFalse(); +}); + +it('filters assigned to null when assignedTo is 1', function() { + $query = Mockery::mock(Builder::class); + $query->shouldReceive('whereNull')->with('assignee_id')->andReturnSelf(); + + $model = Mockery::mock(Assignable::class)->makePartial(); + $result = $model->scopeFilterAssignedTo($query, 1); + + expect($result)->toBe($query); +}); + +it('filters assigned to current staff when assignedTo is 2', function() { + $query = Mockery::mock(Builder::class); + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getKey')->andReturn(1); + AdminAuth::shouldReceive('staff')->andReturn($user); + $query->shouldReceive('where')->with('assignee_id', 1)->andReturnSelf(); + + $model = Mockery::mock(Assignable::class)->makePartial(); + $result = $model->scopeFilterAssignedTo($query, 2); + + expect($result)->toBe($query); +}); + +it('filters assigned to not current staff when assignedTo is not 1 or 2', function() { + $query = Mockery::mock(Builder::class); + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getKey')->andReturn(1); + AdminAuth::shouldReceive('staff')->andReturn($user); + $query->shouldReceive('where')->with('assignee_id', '!=', 1)->andReturnSelf(); + + $model = Mockery::mock(Assignable::class)->makePartial(); + $result = $model->scopeFilterAssignedTo($query, 3); + + expect($result)->toBe($query); +}); + +it('filters where unassigned', function() { + $query = Mockery::mock(Builder::class); + $query->shouldReceive('whereNotNull')->with('assignee_group_id')->andReturnSelf(); + $query->shouldReceive('whereNull')->with('assignee_id')->andReturnSelf(); + + $model = Mockery::mock(Assignable::class)->makePartial(); + $result = $model->scopeWhereUnAssigned($query); + + expect($result)->toBe($query); +}); + +it('filters where assigned to specific user', function() { + $query = Mockery::mock(Builder::class); + $assigneeId = 1; + $query->shouldReceive('where')->with('assignee_id', $assigneeId)->andReturnSelf(); + + $model = Mockery::mock(Assignable::class)->makePartial(); + $result = $model->scopeWhereAssignTo($query, $assigneeId); + + expect($result)->toBe($query); +}); + +it('filters where assigned to specific group', function() { + $query = Mockery::mock(Builder::class); + $assigneeGroupId = 1; + $query->shouldReceive('where')->with('assignee_group_id', $assigneeGroupId)->andReturnSelf(); + + $model = Mockery::mock(Assignable::class)->makePartial(); + $result = $model->scopeWhereAssignToGroup($query, $assigneeGroupId); + + expect($result)->toBe($query); +}); + +it('filters where assigned to specific group ids', function() { + $query = Mockery::mock(Builder::class); + $assigneeGroupIds = [1, 2, 3]; + $query->shouldReceive('whereIn')->with('assignee_group_id', $assigneeGroupIds)->andReturnSelf(); + + $model = Mockery::mock(Assignable::class)->makePartial(); + $result = $model->scopeWhereInAssignToGroup($query, $assigneeGroupIds); + + expect($result)->toBe($query); +}); + +it('filters where has auto assign group', function() { + $query = Mockery::mock(Builder::class); + $query->shouldReceive('whereHas')->with('assignee_group', Mockery::on(function($callback) { + $subQuery = Mockery::mock(Builder::class); + $subQuery->shouldReceive('where')->with('auto_assign', 1)->andReturnSelf(); + $callback($subQuery); + + return true; + }))->andReturnSelf(); + + $model = Mockery::mock(Assignable::class)->makePartial(); + $result = $model->scopeWhereHasAutoAssignGroup($query); + + expect($result)->toBe($query); +}); diff --git a/tests/Models/Concerns/HasCustomerTest.php b/tests/Models/Concerns/HasCustomerTest.php new file mode 100644 index 0000000..b259798 --- /dev/null +++ b/tests/Models/Concerns/HasCustomerTest.php @@ -0,0 +1,109 @@ +model = Mockery::mock(HasCustomer::class)->makePartial()->shouldAllowMockingProtectedMethods(); +}); + +it('applies customer scope with customer id as integer', function() { + $query = Mockery::mock(Builder::class); + $relation = Mockery::mock(Relation::class); + $relation->shouldReceive('getQualifiedForeignKeyName')->andReturn('customer_id'); + $this->model->shouldReceive('customer')->andReturn($relation); + $this->model->shouldReceive('getRelationType')->andReturn('belongsTo'); + $query->shouldReceive('where')->with('customer_id', 1)->andReturnSelf(); + + $result = $this->model->scopeApplyCustomer($query, 1); + + expect($result)->toBe($query); +}); + +it('applies customer scope with customer id as model', function() { + $query = Mockery::mock(Builder::class); + $customer = Mockery::mock(Model::class)->makePartial(); + $relation = Mockery::mock(Relation::class); + $customer->shouldReceive('getKey')->andReturn(1); + $relation->shouldReceive('getQualifiedForeignKeyName')->andReturn('customer_id'); + $this->model->shouldReceive('customer')->andReturn($relation); + $this->model->shouldReceive('getRelationType')->andReturn('belongsTo'); + $query->shouldReceive('where')->with('customer_id', 1)->andReturnSelf(); + + $result = $this->model->scopeApplyCustomer($query, $customer); + + expect($result)->toBe($query); +}); + +it('applies customer scope with hasMany relation', function() { + $query = Mockery::mock(Builder::class); + $relation = Mockery::mock(Relation::class); + $relation->shouldReceive('getQualifiedForeignKeyName')->andReturn('customer_id'); + $this->model->shouldReceive('customer')->andReturn($relation); + $this->model->shouldReceive('getRelationType')->andReturn('hasMany'); + $query->shouldReceive('whereHas')->with('customer', Mockery::on(function($callback) { + $subQuery = Mockery::mock(Builder::class); + $subQuery->shouldReceive('where')->with('customer_id', 1)->andReturnSelf(); + $callback($subQuery); + + return true; + }))->andReturnSelf(); + + $result = $this->model->scopeApplyCustomer($query, 1); + + expect($result)->toBe($query); +}); + +it('returns customer relation name from constant', function() { + $model = new class extends Model + { + public const CUSTOMER_RELATION = 'custom_relation'; + + use HasCustomer; + + public function getNameInTest() + { + return $this->getCustomerRelationName(); + } + }; + + $result = $model->getNameInTest(); + + expect($result)->toBe('custom_relation'); +}); + +it('returns default customer relation name', function() { + $reflection = new \ReflectionClass($this->model); + $method = $reflection->getMethod('getCustomerRelationName'); + $method->setAccessible(true); + $result = $method->invoke($this->model); + + expect($result)->toBe('customer'); +}); + +it('returns true for single relation type', function() { + $this->model->shouldReceive('getRelationType')->with('customer')->andReturn('hasOne'); + + $reflection = new \ReflectionClass($this->model); + $method = $reflection->getMethod('customerIsSingleRelationType'); + $method->setAccessible(true); + $result = $method->invoke($this->model); + + expect($result)->toBeTrue(); +}); + +it('returns false for multiple relation type', function() { + $this->model->shouldReceive('getRelationType')->with('customer')->andReturn('hasMany'); + + $reflection = new \ReflectionClass($this->model); + $method = $reflection->getMethod('customerIsSingleRelationType'); + $method->setAccessible(true); + $result = $method->invoke($this->model); + + expect($result)->toBeFalse(); +}); diff --git a/tests/Models/Concerns/SendInviteTest.php b/tests/Models/Concerns/SendInviteTest.php new file mode 100644 index 0000000..d9ada05 --- /dev/null +++ b/tests/Models/Concerns/SendInviteTest.php @@ -0,0 +1,63 @@ +getPurgeableAttributes())->toContain('send_invite'); +}); + +it('restores purged values and sends invite on save when send_invite is true', function() { + Mail::fake(); + + $user = User::factory()->create(); + $user->name = 'Test User'; + $user->send_invite = true; + $user->save(); + + Mail::assertQueued(AnonymousTemplateMailable::class, function($mailable) { + return $mailable->getTemplateCode() === 'igniter.user::mail.invite'; + }); +}); + +it('does not send invite on save when send_invite is false', function() { + Mail::fake(); + + $user = User::factory()->create(); + $user->name = 'Test User'; + $user->send_invite = false; + $user->save(); + + Mail::assertNotQueued(AnonymousTemplateMailable::class, function($mailable) { + return $mailable->getTemplateCode() === 'igniter.user::mail.invite'; + }); +}); + +it('throws exception when mailSendsInvite is called without implementing sendsInviteGetTemplateCode', function() { + $model = Mockery::mock(SendsInvite::class)->makePartial(); + + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The model ['.get_class($model).'] must implement a sendsInviteGetTemplateCode() method.'); + + $model->mailSendInvite(); +}); + +it('updates reset_code, reset_time, and invited_at when SendsInvite is called', function() { + Mail::fake(); + + $user = User::factory()->create(); + $user->name = 'Test User'; + $user->send_invite = true; + $user->save(); + + $user = $user->fresh(); + + expect($user->reset_code)->not->toBeNull() + ->and($user->reset_time)->not->toBeNull() + ->and($user->invited_at)->not->toBeNull(); +}); diff --git a/tests/Models/CustomerGroupTest.php b/tests/Models/CustomerGroupTest.php new file mode 100644 index 0000000..682464d --- /dev/null +++ b/tests/Models/CustomerGroupTest.php @@ -0,0 +1,66 @@ +create(['group_name' => 'VIP']); + $group2 = CustomerGroup::factory()->create(['group_name' => 'Regular']); + + $result = CustomerGroup::getDropdownOptions(); + + expect($result[$group1->getKey()])->toBe('VIP') + ->and($result[$group2->getKey()])->toBe('Regular'); +}); + +it('returns true when group requires approval', function() { + $customerGroup = Mockery::mock(CustomerGroup::class)->makePartial(); + $customerGroup->approval = true; + + $result = $customerGroup->requiresApproval(); + + expect($result)->toBeTrue(); +}); + +it('returns false when group does not require approval', function() { + $customerGroup = Mockery::mock(CustomerGroup::class)->makePartial(); + $customerGroup->approval = false; + + $result = $customerGroup->requiresApproval(); + + expect($result)->toBeFalse(); +}); + +it('returns correct customer count', function() { + $customerGroup = Mockery::mock(CustomerGroup::class)->makePartial(); + $customerGroup->shouldReceive('customers->count')->andReturn(5); + + $result = $customerGroup->getCustomerCountAttribute(null); + + expect($result)->toBe(5); +}); + +it('returns the correct defaultable name', function() { + $customerGroup = Mockery::mock(CustomerGroup::class)->makePartial(); + $customerGroup->group_name = 'VIP'; + + $result = $customerGroup->defaultableName(); + + expect($result)->toBe('VIP'); +}); + +it('configures customer group model correctly', function() { + $customerGroup = new CustomerGroup; + + expect(class_uses_recursive($customerGroup)) + ->toContain(Defaultable::class) + ->and($customerGroup->getTable())->toBe('customer_groups') + ->and($customerGroup->getKeyName())->toBe('customer_group_id') + ->and($customerGroup->timestamps)->toBeTrue() + ->and($customerGroup->getCasts()['approval'])->toBe('boolean') + ->and($customerGroup->relation['hasMany']['customers'])->toBe(Customer::class); +}); diff --git a/tests/Models/CustomerTest.php b/tests/Models/CustomerTest.php new file mode 100644 index 0000000..17e762a --- /dev/null +++ b/tests/Models/CustomerTest.php @@ -0,0 +1,372 @@ +create(['first_name' => 'John', 'last_name' => 'Doe']); + + $result = Customer::getDropdownOptions(); + + expect($result[$customer->getKey()])->toBe('John Doe'); +}); + +it('returns empty array when no enabled customers are present', function() { + $result = Customer::getDropdownOptions(); + + expect($result)->toBeEmpty(); +}); + +it('returns full name as concatenation of first and last name', function() { + $customer = new Customer; + $customer->first_name = 'John'; + $customer->last_name = 'Doe'; + + expect($customer->full_name)->toBe('John Doe'); +}); + +it('returns email in lowercase', function() { + $customer = new Customer; + $customer->email = 'John.Doe@Example.com'; + + expect($customer->email)->toBe('john.doe@example.com'); +}); + +it('returns when group does not require approval', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $group = Mockery::mock(CustomerGroup::class)->makePartial(); + $group->shouldReceive('requiresApproval')->andReturnFalse(); + $customer->group = $group; + + $result = $customer->beforeLogin(); + + expect($result)->toBeNull(); +}); + +it('returns when customer is activated and enabled', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $group = Mockery::mock(CustomerGroup::class)->makePartial(); + $group->shouldReceive('requiresApproval')->andReturnTrue(); + $customer->is_activated = true; + $customer->group = $group; + $customer->shouldReceive('isEnabled')->andReturnTrue(); + + $result = $customer->beforeLogin(); + + expect($result)->toBeNull(); +}); + +it('throws exception if customer group requires approval and customer is not activated', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $group = Mockery::mock(CustomerGroup::class)->makePartial(); + $group->shouldReceive('requiresApproval')->andReturnTrue(); + $customer->group = $group; + $customer->is_activated = false; + $customer->shouldReceive('isEnabled')->andReturnTrue(); + + expect(fn() => $customer->beforeLogin())->toThrow(SystemException::class); +}); + +it('updates last login timestamp after login', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('saveQuietly')->once(); + + $customer->afterLogin(); + + expect($customer->last_login)->toBeInstanceOf(\Illuminate\Support\Carbon::class); +}); + +it('extends user query to include only enabled users', function() { + $query = Mockery::mock(Builder::class); + $query->shouldReceive('isEnabled')->once(); + + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->extendUserQuery($query); +}); + +it('returns true when customer is enabled', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('isEnabled')->andReturnTrue(); + + $result = $customer->enabled(); + + expect($result)->toBeTrue(); +}); + +it('returns addresses grouped by their keys', function() { + $address1 = Mockery::mock(Address::class)->makePartial(); + $address1->shouldReceive('getKey')->andReturn(1); + $address2 = Mockery::mock(Address::class)->makePartial(); + $address2->shouldReceive('getKey')->andReturn(2); + + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('addresses->get')->andReturn(collect([$address1, $address2])); + + $result = $customer->listAddresses(); + + expect($result->keys()->all())->toBe([1, 2]); +}); + +it('returns empty collection when no addresses are present', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('addresses->get')->andReturn(collect()); + + $result = $customer->listAddresses(); + + expect($result->isEmpty())->toBeTrue(); +}); + +it('returns all customer registration dates', function() { + $dates = ['2023-01-01', '2023-02-01']; + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('pluckDates')->with('created_at')->andReturn($dates); + + $result = $customer->getCustomerDates(); + + expect($result)->toBe($dates); +}); + +it('returns empty array when no registration dates are present', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('pluckDates')->with('created_at')->andReturn([]); + + $result = $customer->getCustomerDates(); + + expect($result)->toBe([]); +}); + +it('saves addresses and deletes old addresses not in the list', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('getKey')->andReturn(1); + $customer->shouldReceive('addresses->updateOrCreate')->andReturnUsing(function($attributes, $values) { + $address = Mockery::mock(Address::class); + $address->shouldReceive('getKey')->andReturn(1); + + return $address; + }); + $customer->shouldReceive('addresses->whereNotIn->delete')->once(); + + $addresses = [ + ['address_id' => 1, 'country_id' => 1, 'address' => '123 Street'], + ['address_id' => 2, 'address' => '456 Avenue'], + ]; + + $customer->saveAddresses($addresses); +}); + +it('returns false when saving addresses if customer key is not numeric', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('getKey')->andReturn(null); + + $result = $customer->saveAddresses([]); + + expect($result)->toBeFalse(); +}); + +it('throws exception when saving default address if address does not belong to customer', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('addresses->find')->andReturn(null); + + expect(fn() => $customer->saveDefaultAddress(1))->toThrow(\Igniter\Flame\Exception\ApplicationException::class); +}); + +it('sets default address for customer', function() { + $address = Mockery::mock(Address::class)->makePartial(); + $address->shouldReceive('getKey')->andReturn(1); + + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('addresses->find')->andReturn($address); + $customer->shouldReceive('save')->once(); + + $result = $customer->saveDefaultAddress(1); + + expect($result->address_id)->toBe(1); +}); + +it('deletes customer address if it belongs to the customer', function() { + $address = Mockery::mock(Address::class); + $address->shouldReceive('delete')->once(); + + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('addresses->find')->andReturn($address); + + $result = $customer->deleteCustomerAddress(1); + + expect($result)->toBe($address); +}); + +it('updates guest orders, address and reservations matching customer email', function() { + $customer = Customer::factory()->create(); + $customer->email = 'john.doe@example.com'; + $address = Address::factory()->create(); + $reservation = Reservation::factory()->create(['email' => $customer->email]); + $order = Order::factory()->create([ + 'customer_id' => null, + 'email' => $customer->email, + 'address_id' => $address->getKey(), + ]); + + $customerId = $customer->getKey(); + $result = $customer->saveCustomerGuestOrder(); + + expect($result)->toBeTrue() + ->and($reservation->fresh()->customer_id)->toBe($customerId) + ->and($order->fresh()->customer_id)->toBe($customerId) + ->and($address->fresh()->customer_id)->toBe($customerId); +}); + +it('sends invite email to customer', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('mailSend')->with('igniter.user::mail.invite_customer', 'customer', [])->once(); + + $customer->mailSendInvite(); +}); + +it('sends reset password request email with default links', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('mailSend')->with('igniter.user::mail.password_reset_request', 'customer', [ + 'reset_link' => null, + 'account_login_link' => null, + ])->once(); + + $customer->mailSendResetPasswordRequest(); +}); + +it('sends reset password email with default login link', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('mailSend')->with('igniter.user::mail.password_reset', 'customer', [ + 'account_login_link' => null, + ])->once(); + + $customer->mailSendResetPassword(); +}); + +it('sends registration email to customer and admin if settings allow', function() { + $vars = [ + 'account_login_link' => null, + ]; + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('mailSend')->with('igniter.user::mail.registration', 'customer', $vars)->once(); + $customer->shouldReceive('mailSend')->with('igniter.user::mail.registration_alert', 'admin', $vars)->once(); + $customer->shouldReceive('fresh')->andReturnSelf(); + + setting()->set(['registration_email' => ['customer', 'admin']]); + + $customer->mailSendRegistration(); +}); + +it('sends email verification email to customer', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('fresh')->andReturnSelf(); + $customer->shouldReceive('mailSend')->with('igniter.user::mail.activation', 'customer', [ + 'account_activation_link' => null, + ])->once(); + + $customer->mailSendEmailVerification(['account_activation_link' => null]); +}); + +it('returns correct recipients for customer type', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->email = 'john.doe@example.com'; + $customer->first_name = 'John'; + $customer->last_name = 'Doe'; + + $result = $customer->mailGetRecipients('customer'); + + expect($result)->toBe([['john.doe@example.com', 'John Doe']]); +}); + +it('returns correct recipients for admin type', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + + setting()->set(['site_email' => 'admin@example.com', 'site_name' => 'Admin']); + + $result = $customer->mailGetRecipients('admin'); + + expect($result)->toBe([['admin@example.com', 'Admin']]); +}); + +it('returns empty array for unknown recipient type', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + + $result = $customer->mailGetRecipients('unknown'); + + expect($result)->toBe([]); +}); + +it('returns correct mail data', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('fresh')->andReturnSelf(); + $customer->email = 'john.doe@example.com'; + $customer->first_name = 'John'; + $customer->last_name = 'Doe'; + + $result = $customer->mailGetData(); + + expect($result['email'])->toBe('john.doe@example.com') + ->and($result['full_name'])->toBe('John Doe') + ->and($result['customer'])->toBe($customer); +}); + +it('registers a new customer and activates if specified', function() { + $attributes = ['first_name' => 'John', 'last_name' => 'Doe', 'email' => 'john.doe@example.com']; + + $result = (new Customer)->register($attributes, true); + + expect($result->first_name)->toBe('John') + ->and($result->last_name)->toBe('Doe') + ->and($result->email)->toBe('john.doe@example.com') + ->and($result->password)->toBeNull(); +}); + +it('returns correct broadcast notification channel', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('getKey')->andReturn(1); + + $result = $customer->receivesBroadcastNotificationsOn(); + + expect($result)->toBe('main.users.1'); +}); + +it('configures customer model correctly', function() { + $customer = new Customer; + + expect(class_uses_recursive($customer)) + ->toContain(Purgeable::class) + ->toContain(SendsInvite::class) + ->toContain(SendsMailTemplate::class) + ->toContain(Switchable::class) + ->and($customer->getTable())->toBe('customers') + ->and($customer->getKeyName())->toBe('customer_id') + ->and($customer->getGuarded())->toBe(['reset_code', 'activation_code', 'remember_token']) + ->and($customer->getHidden())->toBe(['password', 'remember_token']) + ->and($customer->getAppends())->toBe(['full_name']) + ->and($customer->getCasts()['customer_id'])->toBe('integer') + ->and($customer->getCasts()['password'])->toBe('hashed') + ->and($customer->getCasts()['address_id'])->toBe('integer') + ->and($customer->getCasts()['customer_group_id'])->toBe('integer') + ->and($customer->getCasts()['newsletter'])->toBe('boolean') + ->and($customer->getCasts()['is_activated'])->toBe('boolean') + ->and($customer->getCasts()['last_login'])->toBe('datetime') + ->and($customer->getCasts()['invited_at'])->toBe('datetime') + ->and($customer->getCasts()['activated_at'])->toBe('datetime') + ->and($customer->getCasts()['reset_time'])->toBe('datetime') + ->and($customer->relation['hasMany']['addresses'])->toBe([Address::class, 'delete' => true]) + ->and($customer->relation['hasMany']['orders'])->toBe([Order::class]) + ->and($customer->relation['hasMany']['reservations'])->toBe([Reservation::class]) + ->and($customer->relation['belongsTo']['group'])->toBe([CustomerGroup::class, 'foreignKey' => 'customer_group_id']) + ->and($customer->relation['belongsTo']['address'])->toBe(Address::class) + ->and($customer->getPurgeableAttributes())->toBe(['addresses', 'send_invite']); +}); diff --git a/tests/Models/NotificationTest.php b/tests/Models/NotificationTest.php new file mode 100644 index 0000000..65d4072 --- /dev/null +++ b/tests/Models/NotificationTest.php @@ -0,0 +1,56 @@ +data = ['title' => 'Test Title']; + + expect($notification->title)->toEqual('Test Title'); +}); + +it('can get message attribute', function() { + $notification = new Notification; + $notification->data = ['message' => 'Test Message']; + + expect($notification->message)->toEqual('Test Message'); +}); + +it('can get url attribute', function() { + $notification = new Notification; + $notification->data = ['url' => 'http://localhost']; + + expect($notification->url)->toEqual('http://localhost'); +}); + +it('can get icon attribute', function() { + $notification = new Notification; + $notification->data = ['icon' => 'fa fa-bell']; + + expect($notification->icon)->toEqual('fa fa-bell'); +}); + +it('can get icon color attribute', function() { + $notification = new Notification; + $notification->data = ['iconColor' => '#ff0000']; + + expect($notification->iconColor)->toEqual('#ff0000'); +}); + +it('can scope where notifiable', function() { + $query = Notification::query(); + $notifiable = User::factory()->create(); + + $query = $query->whereNotifiable($notifiable); + + expect($query->toSql())->toContain('where (`notifications`.`notifiable_type` = ? and `notifications`.`notifiable_id` = ?)'); +}); + +it('can prune notifications', function() { + $query = (new Notification)->prunable(); + + expect($query->toSql())->toContain('`read_at` is not null and `read_at` <= ?'); +}); diff --git a/tests/Models/Observers/CustomerObserverTest.php b/tests/Models/Observers/CustomerObserverTest.php new file mode 100644 index 0000000..306f640 --- /dev/null +++ b/tests/Models/Observers/CustomerObserverTest.php @@ -0,0 +1,51 @@ +makePartial(); + $customer->shouldReceive('saveCustomerGuestOrder')->once(); + + $observer = new CustomerObserver; + $observer->created($customer); +}); + +it('completes activation if group does not require approval and status is true', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $group = Mockery::mock(CustomerGroup::class); + $group->shouldReceive('requiresApproval')->andReturn(false); + $customer->status = true; + $customer->is_activated = null; + $customer->shouldReceive('extendableGet')->with('group')->andReturn($group); + $customer->shouldReceive('restorePurgedValues')->once(); + $customer->shouldReceive('completeActivation')->with(Mockery::type('string'))->once(); + $customer->exists = true; + + $observer = new CustomerObserver; + $observer->saved($customer); +}); + +it('saves addresses if addresses attribute exists', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $customer->shouldReceive('getAttributes')->andReturn(['addresses' => ['address1', 'address2']]); + $customer->shouldReceive('saveAddresses')->with(['address1', 'address2'])->once(); + $customer->exists = true; + + $observer = new CustomerObserver; + $observer->saved($customer); +}); + +it('deletes addresses on deleting', function() { + $customer = Mockery::mock(Customer::class)->makePartial(); + $addresses = Mockery::mock(); + $addresses->shouldReceive('delete')->once(); + $customer->shouldReceive('addresses')->andReturn($addresses); + + $observer = new CustomerObserver; + $observer->deleting($customer); +}); diff --git a/tests/Models/Observers/UserObserverTest.php b/tests/Models/Observers/UserObserverTest.php new file mode 100644 index 0000000..69b8e71 --- /dev/null +++ b/tests/Models/Observers/UserObserverTest.php @@ -0,0 +1,36 @@ +makePartial(); + $user->shouldReceive('groups->detach')->once(); + $user->shouldReceive('locations->detach')->once(); + + $observer = new UserObserver; + $observer->deleting($user); +}); + +it('restores purged values on saved', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('restorePurgedValues')->once(); + $user->exists = true; + + $observer = new UserObserver; + $observer->saved($user); +}); + +it('completes activation if status is true and is_activated is null', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->status = true; + $user->is_activated = null; + $user->shouldReceive('completeActivation')->with(Mockery::type('string'))->once(); + $user->exists = true; + + $observer = new UserObserver; + $observer->saved($user); +}); diff --git a/tests/Models/UserGroupTest.php b/tests/Models/UserGroupTest.php new file mode 100644 index 0000000..49d23c1 --- /dev/null +++ b/tests/Models/UserGroupTest.php @@ -0,0 +1,134 @@ +create(['user_group_name' => 'Group 1']); + $group2 = UserGroup::factory()->create(['user_group_name' => 'Group 2']); + + $result = UserGroup::getDropdownOptions(); + + expect($result[$group1->getKey()])->toBe('Group 1') + ->and($result[$group2->getKey()])->toBe('Group 2'); +}); + +it('returns list of dropdown options with descriptions', function() { + $group1 = UserGroup::factory()->create(['user_group_name' => 'Group 1', 'description' => 'Group 1 description']); + $group2 = UserGroup::factory()->create(['user_group_name' => 'Group 2', 'description' => 'Group 2 description']); + + $result = UserGroup::listDropdownOptions(); + + expect($result[$group1->getKey()])->toBe(['Group 1', 'Group 1 description']) + ->and($result[$group2->getKey()])->toBe(['Group 2', 'Group 2 description']); +}); + +it('returns staff count attribute', function() { + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $userGroup->shouldReceive('getAttribute')->with('users')->andReturn(collect([1, 2, 3])); + + $result = $userGroup->getStaffCountAttribute(null); + + expect($result)->toBe(3); +}); + +it('syncs auto assign status', function() { + UserGroup::factory()->create(['auto_assign' => 1]); + + UserGroup::syncAutoAssignStatus(); + + expect(setting()->getPref('allocator_is_enabled'))->toBeTrue(); +}); + +it('returns default auto assign limit', function() { + $userGroup = new UserGroup(['auto_assign_limit' => null]); + + $result = $userGroup->getAutoAssignLimitAttribute(null); + + expect($result)->toBe(20); +}); + +it('returns custom auto assign limit', function() { + $userGroup = new UserGroup(['auto_assign_limit' => 10]); + + $result = $userGroup->getAutoAssignLimitAttribute(null); + + expect($result)->toBe(10); +}); + +it('returns true if auto assign is enabled', function() { + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $userGroup->auto_assign = true; + + $result = $userGroup->autoAssignEnabled(); + + expect($result)->toBeTrue(); +}); + +it('returns false if auto assign is disabled', function() { + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $userGroup->auto_assign = false; + + $result = $userGroup->autoAssignEnabled(); + + expect($result)->toBeFalse(); +}); + +it('returns list of assignees', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('isEnabled')->andReturnTrue()->once(); + $user->shouldReceive('canAssignTo')->andReturnTrue()->once(); + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $userGroup->shouldReceive('getAttribute')->with('users')->andReturn(collect([$user])); + + $userGroup->listAssignees(); +}); + +it('returns available assignee using round robin', function() { + $user = Mockery::mock(User::class)->makePartial(); + $query = Mockery::mock(Builder::class)->makePartial(); + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $userGroup->auto_assign_mode = UserGroup::AUTO_ASSIGN_ROUND_ROBIN; + $userGroup->shouldReceive('assignable_logs->newQuery')->andReturn($query); + $query->shouldReceive('applyRoundRobinScope')->andReturnSelf()->once(); + $query->shouldReceive('pluck')->andReturn(collect([1 => 0]))->once(); + $userGroup->shouldReceive('listAssignees')->andReturn(collect([$user]))->once(); + + $result = $userGroup->findAvailableAssignee(); + + expect($result)->toBe($user); +}); + +it('returns available assignee using load balanced', function() { + $user = Mockery::mock(User::class)->makePartial(); + $query = Mockery::mock(Builder::class)->makePartial(); + $userGroup = Mockery::mock(UserGroup::class)->makePartial(); + $userGroup->auto_assign_mode = UserGroup::AUTO_ASSIGN_LOAD_BALANCED; + $userGroup->shouldReceive('assignable_logs->newQuery')->andReturn($query); + $query->shouldReceive('applyLoadBalancedScope')->andReturnSelf()->once(); + $query->shouldReceive('pluck')->andReturn(collect([1 => 0]))->once(); + $userGroup->shouldReceive('listAssignees')->andReturn(collect([$user]))->once(); + + $result = $userGroup->findAvailableAssignee(); + + expect($result)->toBe($user); +}); + +it('configures user group model correctly', function() { + $userGroup = new UserGroup; + + expect($userGroup->getTable())->toBe('admin_user_groups') + ->and($userGroup->getKeyName())->toBe('user_group_id') + ->and($userGroup->timestamps)->toBeTrue() + ->and($userGroup->relation['hasMany']['assignable_logs'])->toBe([AssignableLog::class, 'foreignKey' => 'assignee_group_id']) + ->and($userGroup->relation['belongsToMany']['users'])->toBe([User::class, 'table' => 'admin_users_groups']) + ->and($userGroup->getCasts()['auto_assign'])->toBe('boolean') + ->and($userGroup->getCasts()['auto_assign_mode'])->toBe('integer') + ->and($userGroup->getCasts()['auto_assign_limit'])->toBe('integer') + ->and($userGroup->getCasts()['auto_assign_availability'])->toBe('boolean'); +}); diff --git a/tests/Models/UserPreferenceTest.php b/tests/Models/UserPreferenceTest.php new file mode 100644 index 0000000..26325b4 --- /dev/null +++ b/tests/Models/UserPreferenceTest.php @@ -0,0 +1,133 @@ +getProperty('cache'); + $property->setAccessible(true); + $property->setValue($userPreference, $value); +} + +function getCachePropertyValue(UserPreference $userPreference): array +{ + $reflection = new \ReflectionClass($userPreference); + $property = $reflection->getProperty('cache'); + $property->setAccessible(true); + + return $property->getValue($userPreference); +} + +it('returns default value if user is not set', function() { + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + $userPreference->userContext = null; + + $result = $userPreference->get('theme', 'default'); + + expect($result)->toBe('default'); +}); + +it('returns cached value if available', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->user_id = 1; + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + $userPreference->userContext = $user; + setCachePropertyValue($userPreference, ['1-theme' => 'dark']); + + $result = $userPreference->get('theme', 'default'); + + expect($result)->toBe('dark'); +}); + +it('returns value from database if not cached', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->user_id = 1; + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + $userPreference->userContext = $user; + $userPreference->shouldReceive('findRecord')->with('theme', $user)->andReturn((object)['value' => 'dark']); + setCachePropertyValue($userPreference, []); + + $result = $userPreference->get('theme', 'default'); + + expect($result)->toBe('dark'); +}); + +it('sets returns default when no user', function() { + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + $userPreference->userContext = null; + + expect($userPreference->set('theme', 'dark'))->toBeFalse(); +}); + +it('sets and caches the value', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->user_id = 1; + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + $userPreference->userContext = $user; + $userPreference->shouldReceive('findRecord')->with('theme', $user)->andReturnSelf(); + $userPreference->shouldReceive('save')->andReturnTrue(); + + $result = $userPreference->set('theme', 'dark'); + $userPreferenceCache = getCachePropertyValue($userPreference); + + expect($result)->toBeTrue()->and($userPreferenceCache['1-theme'])->toBe('dark'); +}); + +it('resets returns false when no user', function() { + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + $userPreference->userContext = null; + + expect($userPreference->reset('theme'))->toBeFalse(); +}); + +it('resets returns false when no record', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->user_id = 1; + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + $userPreference->userContext = $user; + $userPreference->shouldReceive('findRecord')->with('theme', $user)->andReturnNull(); + + expect($userPreference->reset('theme'))->toBeFalse(); +}); + +it('resets the value and removes from cache', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->user_id = 1; + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + $userPreference->userContext = $user; + $userPreference->shouldReceive('findRecord')->with('theme', $user)->andReturnSelf(); + setCachePropertyValue($userPreference, ['1-theme' => 'dark']); + + $result = $userPreference->reset('theme'); + $userPreferenceCache = getCachePropertyValue($userPreference); + + expect($result)->toBeTrue() + ->and(array_key_exists('1-theme', $userPreferenceCache))->toBeFalse(); +}); + +it('returns user if logged in', function() { + $user = Mockery::mock(User::class)->makePartial(); + AdminAuth::shouldReceive('getUser')->andReturn($user); + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + + $result = $userPreference->resolveUser(); + + expect($result)->toBe($user); +}); + +it('throws exception if user is not logged in', function() { + AdminAuth::shouldReceive('getUser')->andReturn(null); + $userPreference = Mockery::mock(UserPreference::class)->makePartial(); + + $this->expectException(SystemException::class); + $this->expectExceptionMessage('User is not logged in'); + + $userPreference->resolveUser(); +}); diff --git a/tests/Models/UserRoleTest.php b/tests/Models/UserRoleTest.php new file mode 100644 index 0000000..dd55019 --- /dev/null +++ b/tests/Models/UserRoleTest.php @@ -0,0 +1,78 @@ +create(['name' => 'Role 1']); + $role2 = UserRole::factory()->create(['name' => 'Role 2']); + + $result = UserRole::getDropdownOptions(); + + expect($result[$role1->getKey()])->toBe('Role 1') + ->and($result[$role2->getKey()])->toBe('Role 2'); +}); + +it('returns list of dropdown options with descriptions', function() { + $role1 = UserRole::factory()->create(['name' => 'Role 1', 'description' => 'Role 1 description']); + $role2 = UserRole::factory()->create(['name' => 'Role 2', 'description' => 'Role 2 description']); + + $result = UserRole::listDropdownOptions(); + + expect($result[$role1->getKey()])->toBe(['Role 1', 'Role 1 description']) + ->and($result[$role2->getKey()])->toBe(['Role 2', 'Role 2 description']); +}); + +it('returns staff count attribute', function() { + $userRole = Mockery::mock(UserRole::class)->makePartial(); + $userRole->shouldReceive('getAttribute')->with('users')->andReturn(collect([1, 2, 3])); + + $result = $userRole->getStaffCountAttribute(null); + + expect($result)->toBe(3); +}); + +it('throws exception for invalid permission value', function() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid value "2" for permission "edit" given.'); + + $userRole = Mockery::mock(UserRole::class)->makePartial(); + $userRole->setPermissionsAttribute(['edit' => 2]); +}); + +it('sets permissions attribute correctly', function() { + $userRole = Mockery::mock(UserRole::class)->makePartial(); + $permissions = ['edit' => 1, 'delete' => -1]; + $userRole->setPermissionsAttribute($permissions); + + $result = $userRole->getAttribute('permissions'); + + expect($result)->toBe($permissions); +}); + +it('removes permissions with value 0', function() { + $userRole = Mockery::mock(UserRole::class)->makePartial(); + $permissions = ['edit' => 1, 'delete' => 0]; + $userRole->setPermissionsAttribute($permissions); + + $result = $userRole->getAttribute('permissions'); + + expect($result)->toBe(['edit' => 1]); +}); + +it('configures user role model correctly', function() { + $role = new UserRole; + + expect($role->getTable())->toBe('admin_user_roles') + ->and($role->getKeyName())->toBe('user_role_id') + ->and($role->timestamps)->toBeTrue() + ->and($role->relation['hasMany']['users'])->toBe([ + User::class, 'foreignKey' => 'user_role_id', 'otherKey' => 'user_role_id', + ]) + ->and($role->getCasts()['permissions'])->toBe(Serialize::class); +}); diff --git a/tests/Models/UserTest.php b/tests/Models/UserTest.php new file mode 100644 index 0000000..710cc0b --- /dev/null +++ b/tests/Models/UserTest.php @@ -0,0 +1,404 @@ +makePartial(); + $user->name = 'John Doe'; + + expect($user->getStaffNameAttribute())->toBe('John Doe') + ->and($user->getFullNameAttribute(null))->toBe('John Doe'); +}); + +it('returns correct email', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->email = 'user@example.com'; + + $result = $user->getStaffEmailAttribute(); + + expect($result)->toBe('user@example.com'); +}); + +it('returns correct avatar URL', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->email = 'user@example.com'; + + $result = $user->getAvatarUrlAttribute(); + + expect($result)->toBe('//www.gravatar.com/avatar/'.md5('user@example.com').'.png?d=mm'); +}); + +it('returns default sale permission when not set', function() { + $user = Mockery::mock(User::class)->makePartial(); + + $result = $user->getSalePermissionAttribute(null); + + expect($result)->toBe(1); +}); + +it('returns correct sale permission when set', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->sale_permission = 2; + + $result = $user->getSalePermissionAttribute(2); + + expect($result)->toBe(2); +}); + +it('returns correct dropdown options for enabled users', function() { + $user1 = User::factory()->create(['name' => 'John Doe', 'status' => 1]); + $user2 = User::factory()->create(['name' => 'Jane Doe', 'status' => 1]); + + $result = User::getDropdownOptions(); + + expect($result[$user1->getKey()])->toBe('John Doe') + ->and($result[$user2->getKey()])->toBe('Jane Doe'); +}); + +it('returns empty array when no enabled users are present', function() { + $result = User::getDropdownOptions(); + + expect($result)->toBeEmpty(); +}); + +it('filters out super users', function() { + $query = Mockery::mock(Builder::class); + $query->shouldReceive('where')->with('super_user', '!=', 1)->andReturnSelf(); + $query->shouldReceive('orWhereNull')->with('super_user')->andReturnSelf()->once(); + + $user = Mockery::mock(User::class)->makePartial(); + $user->scopeWhereNotSuperUser($query); +}); + +it('filters only super users', function() { + $query = Mockery::mock(Builder::class); + $query->shouldReceive('where')->with('super_user', 1)->andReturnSelf()->once(); + + $user = Mockery::mock(User::class)->makePartial(); + $user->scopeWhereIsSuperUser($query); +}); + +it('updates last login timestamp after login', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('query')->andReturnSelf(); + $user->shouldReceive('whereKey')->andReturnSelf(); + $user->shouldReceive('update')->with(Mockery::on(function($callback) { + return $callback['last_login'] instanceof Carbon; + }))->once(); + + $user->afterLogin(); +}); + +it('returns true for super user', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->super_user = 1; + + $result = $user->isSuperUser(); + + expect($result)->toBeTrue(); +}); + +it('returns false for non-super user', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->super_user = 0; + + $result = $user->isSuperUser(); + + expect($result)->toBeFalse(); +}); + +it('returns true if user is super user', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('isSuperUser')->andReturnTrue(); + + $result = $user->hasAnyPermission('any_permission'); + + expect($result)->toBeTrue(); +}); + +it('returns true if user has any of the given permissions', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('isSuperUser')->andReturnFalse(); + $user->shouldReceive('getPermissions')->andReturn(['permission1', 'permission2']); + $permissionManager = Mockery::mock(PermissionManager::class); + $permissionManager->shouldReceive('checkPermission')->with(['permission1', 'permission2'], ['permission1'], false)->andReturnTrue(); + app()->instance(PermissionManager::class, $permissionManager); + + $result = $user->hasAnyPermission('permission1'); + + expect($result)->toBeTrue(); +}); + +it('returns false if user does not have any of the given permissions', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('isSuperUser')->andReturnFalse(); + $user->shouldReceive('getPermissions')->andReturn(['permission1', 'permission2']); + $permissionManager = Mockery::mock(PermissionManager::class); + $permissionManager->shouldReceive('checkPermission')->with(['permission1', 'permission2'], ['permission3'], false)->andReturnFalse(); + app()->instance(PermissionManager::class, $permissionManager); + + $result = $user->hasAnyPermission('permission3'); + + expect($result)->toBeFalse(); +}); + +it('returns true if user has all of the given permissions', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('isSuperUser')->andReturnFalse(); + $user->shouldReceive('getPermissions')->andReturn(['permission1', 'permission2']); + $permissionManager = Mockery::mock(PermissionManager::class); + $permissionManager->shouldReceive('checkPermission')->with(['permission1', 'permission2'], ['permission1', 'permission2'], true)->andReturnTrue(); + app()->instance(PermissionManager::class, $permissionManager); + + $result = $user->hasPermission('permission1,permission2', true); + + expect($result)->toBeTrue(); +}); + +it('returns false if user does not have all of the given permissions', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('isSuperUser')->andReturnFalse(); + $user->shouldReceive('getPermissions')->andReturn(['permission1', 'permission2']); + $permissionManager = Mockery::mock(PermissionManager::class); + $permissionManager->shouldReceive('checkPermission')->with(['permission1', 'permission2'], ['permission1', 'permission3'], true)->andReturnFalse(); + app()->instance(PermissionManager::class, $permissionManager); + + $result = $user->hasPermission('permission1,permission3', true); + + expect($result)->toBeFalse(); +}); + +it('returns correct permissions for user role', function() { + $role = Mockery::mock(UserRole::class)->makePartial(); + $role->permissions = ['permission1', 'permission2']; + + $user = Mockery::mock(User::class)->makePartial(); + $user->role = $role; + + $result = $user->getPermissions(); + + expect($result)->toBe(['permission1', 'permission2']); +}); + +it('returns empty permissions when user role is not set', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->role = null; + + $result = $user->getPermissions(); + + expect($result)->toBe([]); +}); + +it('returns staff/site email and name when type is staff or admin', function() { + setting()->set(['site_email' => 'admin@example.com', 'site_name' => 'Admin Name']); + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('extendableGet')->with('full_name')->andReturn('Staff Name'); + $user->email = 'staff@example.com'; + + $result = $user->mailGetRecipients('staff'); + + expect($result)->toBe([['staff@example.com', 'Staff Name']]); + + $result = $user->mailGetRecipients('admin'); + + expect($result)->toBe([['admin@example.com', 'Admin Name']]); + + $result = $user->mailGetRecipients('unknown'); + + expect($result)->toBe([]); +}); + +it('returns true if user can be assigned to', function() { + $user = Mockery::mock(User::class)->makePartial(); + $result = $user->canAssignTo(); + + expect($result)->toBeTrue(); +}); + +it('returns true if user has global assignable scope', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->sale_permission = 1; + + $result = $user->hasGlobalAssignableScope(); + + expect($result)->toBeTrue(); +}); + +it('returns false if user does not have global assignable scope', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->sale_permission = 2; + + $result = $user->hasGlobalAssignableScope(); + + expect($result)->toBeFalse(); +}); + +it('returns true if user has group assignable scope', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->sale_permission = 2; + + $result = $user->hasGroupAssignableScope(); + + expect($result)->toBeTrue(); +}); + +it('returns false if user does not have group assignable scope', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->sale_permission = 1; + + $result = $user->hasGroupAssignableScope(); + + expect($result)->toBeFalse(); +}); + +it('returns true if user has restricted assignable scope', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->sale_permission = 3; + + $result = $user->hasRestrictedAssignableScope(); + + expect($result)->toBeTrue(); +}); + +it('returns false if user does not have restricted assignable scope', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->sale_permission = 1; + + $result = $user->hasRestrictedAssignableScope(); + + expect($result)->toBeFalse(); +}); + +it('returns user creation dates', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('pluckDates')->with('created_at')->andReturn(['2023-01-01', '2023-02-01']); + + $result = $user->getUserDates(); + + expect($result)->toBe(['2023-01-01', '2023-02-01']); +}); + +it('syncs locations successfully', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('locations->sync')->with([1, 2, 3])->andReturnTrue(); + + $result = $user->addLocations([1, 2, 3]); + + expect($result)->toBeTrue(); +}); + +it('syncs groups successfully', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('groups->sync')->with([1, 2, 3])->andReturnTrue(); + + $result = $user->addGroups([1, 2, 3]); + + expect($result)->toBeTrue(); +}); + +it('registers a new user and activates it', function() { + $attributes = [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'username' => 'johndoe', + 'password' => 'secret', + 'language_id' => 1, + 'user_role_id' => 1, + 'super_user' => false, + 'status' => true, + 'groups' => [1, 2], + 'locations' => [1, 2], + ]; + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('save')->andReturnTrue()->once(); + $user->shouldReceive('completeActivation')->andReturnTrue()->never(); + $user->shouldReceive('groups->attach')->with([1, 2])->andReturnTrue(); + $user->shouldReceive('locations->attach')->with([1, 2])->andReturnTrue(); + $user->shouldReceive('reload')->andReturnSelf(); + + $user->register($attributes); +}); + +it('registers a new user without activation', function() { + $attributes = [ + 'name' => 'Jane Doe', + 'email' => 'jane@example.com', + 'username' => 'janedoe', + 'password' => 'secret', + 'language_id' => 1, + 'user_role_id' => 1, + 'super_user' => false, + 'status' => true, + 'groups' => [1, 2], + 'locations' => [1, 2], + ]; + + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('save')->andReturnTrue()->once(); + $user->shouldReceive('completeActivation')->andReturnTrue()->once(); + $user->shouldReceive('groups->attach')->with([1, 2])->andReturnTrue(); + $user->shouldReceive('locations->attach')->with([1, 2])->andReturnTrue(); + $user->shouldReceive('reload')->andReturnSelf(); + + $user->register($attributes, true); +}); + +it('returns broadcast notification channel', function() { + $user = Mockery::mock(User::class)->makePartial(); + $user->shouldReceive('getKey')->andReturn(123); + + $result = $user->receivesBroadcastNotificationsOn(); + + expect($result)->toBe('admin.users.123'); +}); + +it('configures user model correctly', function() { + $user = new User; + + expect(class_uses_recursive($user)) + ->toContain(Locationable::class) + ->toContain(Purgeable::class) + ->toContain(SendsInvite::class) + ->toContain(SendsMailTemplate::class) + ->toContain(Switchable::class) + ->and($user->getTable())->toBe('admin_users') + ->and($user->getKeyName())->toBe('user_id') + ->and($user->timestamps)->toBeTrue() + ->and($user->getFillable())->toBe(['username', 'super_user']) + ->and($user->getAppends())->toBe(['full_name']) + ->and($user->getHidden())->toBe(['password', 'remember_token']) + ->and($user->getCasts()['password'])->toBe('hashed') + ->and($user->getCasts()['user_role_id'])->toBe('integer') + ->and($user->getCasts()['sale_permission'])->toBe('integer') + ->and($user->getCasts()['language_id'])->toBe('integer') + ->and($user->getCasts()['super_user'])->toBe('boolean') + ->and($user->getCasts()['is_activated'])->toBe('boolean') + ->and($user->getCasts()['reset_time'])->toBe('datetime') + ->and($user->getCasts()['invited_at'])->toBe('datetime') + ->and($user->getCasts()['activated_at'])->toBe('datetime') + ->and($user->getCasts()['last_login'])->toBe('datetime') + ->and($user->relation['hasMany']['assignable_logs'])->toBe([AssignableLog::class, 'foreignKey' => 'assignee_id']) + ->and($user->relation['belongsTo']['role'])->toBe([UserRole::class, 'foreignKey' => 'user_role_id']) + ->and($user->relation['belongsTo']['language'])->toBe([Language::class]) + ->and($user->relation['belongsToMany']['groups'])->toBe([UserGroup::class, 'table' => 'admin_users_groups']) + ->and($user->relation['morphToMany']['locations'])->toBe([Location::class, 'name' => 'locationable']) + ->and($user->getCasts()['super_user'])->toBe('boolean') + ->and($user->getCasts()['sale_permission'])->toBe('integer'); +}); diff --git a/tests/Notifications/AssigneeUpdatedNotificationTest.php b/tests/Notifications/AssigneeUpdatedNotificationTest.php new file mode 100644 index 0000000..6711fe7 --- /dev/null +++ b/tests/Notifications/AssigneeUpdatedNotificationTest.php @@ -0,0 +1,133 @@ +subject = Mockery::mock(AssignableLog::class)->makePartial(); + $this->notification = AssigneeUpdatedNotification::make()->subject($this->subject); +}); + +it('returns recipients when assignee is null and assignee group is set', function() { + $user1 = Mockery::mock(User::class)->makePartial(); + $user2 = Mockery::mock(User::class)->makePartial(); + $assignable = Mockery::mock(Order::class)->makePartial(); + $this->subject->shouldReceive('extendableGet')->with('assignee')->andReturnNull(); + $this->subject->shouldReceive('extendableGet')->with('assignee_group')->andReturn(Mockery::mock()); + $this->subject->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + $assignable->shouldReceive('listGroupAssignees')->andReturn(collect([$user1, $user2])); + $user1->shouldReceive('getKey')->andReturn(1); + $user2->shouldReceive('getKey')->andReturn(2); + AdminAuth::shouldReceive('user')->andReturnSelf(); + AdminAuth::shouldReceive('getKey')->andReturn(2); + + $result = $this->notification->getRecipients(); + + expect($result)->toHaveCount(1); +}); + +it('returns recipients when assignee is set', function() { + $this->subject->shouldReceive('extendableGet')->with('assignee')->andReturn(Mockery::mock()); + + $result = $this->notification->getRecipients(); + + expect($result)->toHaveCount(1) + ->and($result[0])->toBe($this->subject->assignee); +}); + +it('returns title for order', function() { + $assignable = Mockery::mock(Order::class)->makePartial(); + $this->subject->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + + $result = $this->notification->getTitle(); + + expect($result)->toBe(lang('igniter.cart::default.orders.notify_assigned_title')); +}); + +it('returns title for reservation', function() { + $assignable = Mockery::mock(Reservation::class)->makePartial(); + $this->subject->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + + $result = $this->notification->getTitle(); + + expect($result)->toBe(lang('igniter.reservation::default.notify_assigned_title')); +}); + +it('returns URL for order', function() { + $assignable = Mockery::mock(Order::class)->makePartial(); + $assignable->shouldReceive('getKey')->andReturn(1); + $this->subject->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + + $result = $this->notification->getUrl(); + + expect($result)->toBe(admin_url('orders/edit/1')); +}); + +it('returns URL for reservation', function() { + $assignable = Mockery::mock(Reservation::class)->makePartial(); + $assignable->shouldReceive('getKey')->andReturn(1); + $this->subject->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + + $result = $this->notification->getUrl(); + + expect($result)->toBe(admin_url('reservations/edit/1')); +}); + +it('returns message for order', function() { + $user = Mockery::mock(User::class)->makePartial(); + $assignable = Mockery::mock(Order::class)->makePartial(); + $assignable->shouldReceive('getKey')->andReturn(1); + $user->shouldReceive('extendableGet')->with('full_name')->andReturn('John Doe'); + $this->subject->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + $this->subject->shouldReceive('extendableGet')->with('assignee')->andReturn(Mockery::mock()); + $this->subject->shouldReceive('extendableGet')->with('user')->andReturn($user); + + $result = $this->notification->getMessage(); + + expect($result)->toBe(sprintf( + lang('igniter.cart::default.orders.notify_assigned'), + 'John Doe', + 1, + lang('igniter::admin.text_you'), + )); +}); + +it('returns message for reservation', function() { + $assignable = Mockery::mock(Reservation::class)->makePartial(); + $userGroup = Mockery::mock(UserGroup::class); + $user = Mockery::mock(User::class)->makePartial(); + $assignable->shouldReceive('getKey')->andReturn(1); + $userGroup->shouldReceive('extendableGet')->with('user_group_name')->andReturn('Group A'); + $this->subject->shouldReceive('extendableGet')->with('assignable')->andReturn($assignable); + $this->subject->shouldReceive('extendableGet')->with('assignee_group')->andReturn($userGroup); + $user->shouldReceive('extendableGet')->with('full_name')->andReturn('John Doe'); + $this->subject->shouldReceive('extendableGet')->with('user')->andReturn($user); + + $result = $this->notification->getMessage(); + + expect($result)->toBe(sprintf(lang('igniter.reservation::default.notify_assigned'), 'John Doe', 1, 'Group A')); +}); + +it('returns icon', function() { + $notification = Mockery::mock(AssigneeUpdatedNotification::class)->makePartial(); + + $result = $notification->getIcon(); + + expect($result)->toBe('fa-clipboard-user'); +}); + +it('returns alias', function() { + $notification = Mockery::mock(AssigneeUpdatedNotification::class)->makePartial(); + + $result = $notification->getAlias(); + + expect($result)->toBe('assignee-updated'); +}); diff --git a/tests/Notifications/CustomerRegisteredNotificationTest.php b/tests/Notifications/CustomerRegisteredNotificationTest.php new file mode 100644 index 0000000..37130b1 --- /dev/null +++ b/tests/Notifications/CustomerRegisteredNotificationTest.php @@ -0,0 +1,60 @@ +subject = Mockery::mock(Customer::class)->makePartial(); + $this->notification = CustomerRegisteredNotification::make()->subject($this->subject); +}); + +it('returns recipients who are enabled super users', function() { + $user = User::factory()->superUser()->create(); + + $result = $this->notification->getRecipients(); + + expect($result)->toHaveCount(1) + ->and($result[0]->getKey())->toBe($user->getKey()); +}); + +it('returns title for customer registered notification', function() { + expect($this->notification->getTitle())->toBe(lang('igniter.user::default.login.notify_registered_account_title')); +}); + +it('returns URL for customer edit page', function() { + $this->subject->shouldReceive('getKey')->andReturn(1); + + $result = $this->notification->getUrl(); + + expect($result)->toBe(admin_url('customers/edit/1')); +}); + +it('returns URL for customers page when subject is null', function() { + $result = $this->notification->getUrl(); + + expect($result)->toBe(admin_url('customers/edit')); +}); + +it('returns message for customer registered notification', function() { + $this->subject->shouldReceive('extendableGet')->with('full_name')->andReturn('John Doe'); + + $result = $this->notification->getMessage(); + + expect($result)->toBe(sprintf(lang('igniter.user::default.login.notify_registered_account'), 'John Doe')); +}); + +it('returns icon for customer registered notification', function() { + $result = $this->notification->getIcon(); + + expect($result)->toBe('fa-user'); +}); + +it('returns alias for customer registered notification', function() { + $result = $this->notification->getAlias(); + + expect($result)->toBe('customer-registered'); +}); diff --git a/tests/Pest.php b/tests/Pest.php index b3d9bbc..189d1a4 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -1 +1,38 @@ in(__DIR__); + +function actingAsSuperUser(?User $user = null) +{ + return test()->actingAs($user ?? User::factory()->superUser()->create(), 'igniter-admin'); +} + +function setObjectProtectedProperty($object, $property, $value) +{ + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($property); + $property->setAccessible(true); + $property->setValue($object, $value); +} + +function getObjectProtectedProperty($object, $property) +{ + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($property); + $property->setAccessible(true); + + return $property->getValue($object); +} + +function mockRequest(array $data) +{ + $mockRequest = Mockery::mock(Request::class)->makePartial(); + $mockRequest->shouldReceive('post')->andReturn($data); + $mockRequest->shouldReceive('setUserResolver')->andReturnNull(); + app()->instance('request', $mockRequest); + + return $mockRequest; +} diff --git a/tests/Requests/CustomerGroupTest.php b/tests/Requests/CustomerGroupTest.php deleted file mode 100644 index f246163..0000000 --- a/tests/Requests/CustomerGroupTest.php +++ /dev/null @@ -1,19 +0,0 @@ -rules(), 'group_name'); - - expect('required')->toBeIn($rules) - ->and('between:2,32')->toBeIn($rules); -}); - -it('has rules for description field', function() { - $rules = array_get((new CustomerGroupRequest)->rules(), 'description'); - - expect('string')->toBeIn($rules) - ->and('between:2,512')->toBeIn($rules); -}); diff --git a/tests/Requests/CustomerTest.php b/tests/Requests/CustomerTest.php deleted file mode 100644 index 7fa9734..0000000 --- a/tests/Requests/CustomerTest.php +++ /dev/null @@ -1,27 +0,0 @@ -rules(), 'first_name'); - - expect('required')->toBeIn($rules) - ->and('between:1,48')->toBeIn($rules); -}); - -it('has rules for last_name field', function() { - $rules = array_get((new CustomerRequest)->rules(), 'last_name'); - - expect('required')->toBeIn($rules) - ->and('between:1,48')->toBeIn($rules); -}); - -it('has rules for email field', function() { - $rules = array_get((new CustomerRequest)->rules(), 'email'); - - expect('email:filter')->toBeIn($rules) - ->and('max:96')->toBeIn($rules) - ->and('unique:customers,email')->toBeIn($rules); -})->skip('Update unique rule expectation'); diff --git a/tests/Requests/UserGroupTest.php b/tests/Requests/UserGroupTest.php deleted file mode 100644 index 19a296b..0000000 --- a/tests/Requests/UserGroupTest.php +++ /dev/null @@ -1,19 +0,0 @@ -toBeIn(array_get((new UserGroupRequest)->rules(), 'user_group_name')) - ->and('required')->toBeIn(array_get((new UserGroupRequest)->rules(), 'auto_assign')) - ->and('required_if:auto_assign,true')->toBeIn(array_get((new UserGroupRequest)->rules(), 'auto_assign_mode')) - ->and('required_if:auto_assign_mode,2')->toBeIn(array_get((new UserGroupRequest)->rules(), 'auto_assign_limit')) - ->and('required_if:auto_assign,true')->toBeIn(array_get((new UserGroupRequest)->rules(), 'auto_assign_availability')); -}); - -it('has max characters rule for inputs', function() { - expect('between:2,255')->toBeIn(array_get((new UserGroupRequest)->rules(), 'user_group_name')) - ->and('max:2')->toBeIn(array_get((new UserGroupRequest)->rules(), 'auto_assign_mode')) - ->and('max:99')->toBeIn(array_get((new UserGroupRequest)->rules(), 'auto_assign_limit')); -}); diff --git a/tests/Requests/UserRoleTest.php b/tests/Requests/UserRoleTest.php deleted file mode 100644 index cd5105e..0000000 --- a/tests/Requests/UserRoleTest.php +++ /dev/null @@ -1,24 +0,0 @@ -toBeIn(array_get((new UserRoleRequest)->rules(), 'name')) - ->and('required')->toBeIn(array_get((new UserRoleRequest)->rules(), 'permissions')) - ->and('required')->toBeIn(array_get((new UserRoleRequest)->rules(), 'permissions.*')); -}); - -it('has max characters rule for inputs', function() { - expect('between:2,32')->toBeIn(array_get((new UserRoleRequest)->rules(), 'code')) - ->and('between:2,255')->toBeIn(array_get((new UserRoleRequest)->rules(), 'name')); -}); - -it('has alpha_dash rule for inputs', function() { - expect('alpha_dash')->toBeIn(array_get((new UserRoleRequest)->rules(), 'code')); -}); - -it('has unique:admin_user_roles rule for inputs', function() { - expect('unique:admin_user_roles')->toBeIn(array_get((new UserRoleRequest)->rules(), 'name')); -})->skip(); diff --git a/tests/Requests/UserTest.php b/tests/Requests/UserTest.php deleted file mode 100644 index e2e4889..0000000 --- a/tests/Requests/UserTest.php +++ /dev/null @@ -1,33 +0,0 @@ -toBeIn(array_get((new UserRequest)->rules(), 'name')) - ->and('required')->toBeIn(array_get((new UserRequest)->rules(), 'email')) - ->and('required')->toBeIn(array_get((new UserRequest)->rules(), 'username')) - ->and('required_if:send_invite,0')->toBeIn(array_get((new UserRequest)->rules(), 'password')) - ->and('required')->toBeIn(array_get((new UserRequest)->rules(), 'user_role_id')) - ->and('required')->toBeIn(array_get((new UserRequest)->rules(), 'groups')); -}); - -it('has sometimes rule for inputs', function() { - expect('nullable')->toBeIn(array_get((new UserRequest)->rules(), 'password')) - ->and('sometimes')->toBeIn(array_get((new UserRequest)->rules(), 'user_role_id')) - ->and('sometimes')->toBeIn(array_get((new UserRequest)->rules(), 'groups')); -}); - -it('has max characters rule for inputs', function() { - expect('between:2,255')->toBeIn(array_get((new UserRequest)->rules(), 'name')) - ->and('max:96')->toBeIn(array_get((new UserRequest)->rules(), 'email')) - ->and('email:filter')->toBeIn(array_get((new UserRequest)->rules(), 'email')) - ->and('between:2,32')->toBeIn(array_get((new UserRequest)->rules(), 'username')) - ->and('between:6,32')->toBeIn(array_get((new UserRequest)->rules(), 'password')); -}); - -it('has unique rule for inputs', function() { - expect('unique:admin_users,email')->toBeIn(array_get((new UserRequest)->rules(), 'email')) - ->and('unique:admin_users,username')->toBeIn(array_get((new UserRequest)->rules(), 'username')); -})->skip(); diff --git a/tests/Subscribers/AssigneeUpdatedSubscriberTest.php b/tests/Subscribers/AssigneeUpdatedSubscriberTest.php new file mode 100644 index 0000000..e799d9c --- /dev/null +++ b/tests/Subscribers/AssigneeUpdatedSubscriberTest.php @@ -0,0 +1,26 @@ +subscribe($events); + + expect($result)->toBe(['admin.assignable.assigned' => 'handleAssigned']); +}); + +it('handles assigned event', function() { + $order = Mockery::mock(Order::class)->makePartial(); + $log = Mockery::mock(AssignableLog::class)->makePartial(); + + $subscriber = new AssigneeUpdatedSubscriber; + expect($subscriber->handleAssigned($order, $log))->toBeNull(); +}); diff --git a/tests/Subscribers/ConsoleSubscriberTest.php b/tests/Subscribers/ConsoleSubscriberTest.php new file mode 100644 index 0000000..5a322ae --- /dev/null +++ b/tests/Subscribers/ConsoleSubscriberTest.php @@ -0,0 +1,34 @@ +subscribe($events); + + expect($result)->toBe(['console.schedule' => 'defineSchedule']); +}); + +it('defines schedule for assignables allocation and clearing user state', function() { + $schedule = Mockery::mock(Schedule::class); + $schedule->shouldReceive('command')->with('igniter:allocate-assignables')->andReturnSelf()->once(); + $schedule->shouldReceive('name')->with('Assignables Allocator')->andReturnSelf()->once(); + $schedule->shouldReceive('withoutOverlapping')->with(5)->andReturnSelf(); + $schedule->shouldReceive('runInBackground')->andReturnSelf(); + $schedule->shouldReceive('everyMinute')->andReturnSelf(); + $schedule->shouldReceive('command')->with('igniter:clear-user-state')->andReturnSelf()->once(); + $schedule->shouldReceive('name')->with('Clear user custom away status')->andReturnSelf()->once(); + $schedule->shouldReceive('withoutOverlapping')->with(5)->andReturnSelf(); + $schedule->shouldReceive('runInBackground')->andReturnSelf(); + $schedule->shouldReceive('everyMinute')->andReturnSelf(); + + $subscriber = new ConsoleSubscriber; + $subscriber->defineSchedule($schedule); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php deleted file mode 100644 index e071824..0000000 --- a/tests/TestCase.php +++ /dev/null @@ -1,14 +0,0 @@ -