diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php index ca253cbd..5e5613b9 100644 --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -1,702 +1,699 @@ guard()->user(); $search = trim(request()->input('search')); $page = intval(request()->input('page')) ?: 1; $pageSize = 20; $hasMore = false; $result = $user->users(); // Search by user email, alias or name if (strlen($search) > 0) { // thanks to cloning we skip some extra queries in $user->users() $allUsers1 = clone $result; $allUsers2 = clone $result; $result->whereLike('email', $search) ->union( $allUsers1->join('user_aliases', 'users.id', '=', 'user_aliases.user_id') ->whereLike('alias', $search) ) ->union( $allUsers2->join('user_settings', 'users.id', '=', 'user_settings.user_id') ->whereLike('value', $search) ->whereIn('key', ['first_name', 'last_name']) ); } $result = $result->orderBy('email') ->limit($pageSize + 1) ->offset($pageSize * ($page - 1)) ->get(); if (count($result) > $pageSize) { $result->pop(); $hasMore = true; } // Process the result $result = $result->map( function ($user) { return $this->objectToClient($user); } ); $result = [ 'list' => $result, 'count' => count($result), 'hasMore' => $hasMore, ]; return response()->json($result); } /** * Display information on the user account specified by $id. * * @param string $id The account to show information for. * * @return \Illuminate\Http\JsonResponse */ public function show($id) { $user = User::find($id); if (!$this->checkTenant($user)) { return $this->errorResponse(404); } if (!$this->guard()->user()->canRead($user)) { return $this->errorResponse(403); } $response = $this->userResponse($user); $response['skus'] = \App\Entitlement::objectEntitlementsSummary($user); $response['config'] = $user->getConfig(); $response['aliases'] = $user->aliases()->pluck('alias')->all(); $code = $user->verificationcodes()->where('active', true) ->where('expires_at', '>', \Carbon\Carbon::now()) ->first(); if ($code) { $response['passwordLinkCode'] = $code->short_code . '-' . $code->code; } return response()->json($response); } /** * User status (extended) information * * @param \App\User $user User object * * @return array Status information */ public static function statusInfo($user): array { $process = self::processStateInfo( $user, [ 'user-new' => true, 'user-ldap-ready' => $user->isLdapReady(), 'user-imap-ready' => $user->isImapReady(), ] ); // Check if the user is a controller of his wallet $isController = $user->canDelete($user); $isDegraded = $user->isDegraded(); $hasMeet = !$isDegraded && Sku::withObjectTenantContext($user)->where('title', 'room')->exists(); $hasCustomDomain = $user->wallet()->entitlements() ->where('entitleable_type', Domain::class) ->count() > 0; // Get user's entitlements titles $skus = $user->entitlements()->select('skus.title') ->join('skus', 'skus.id', '=', 'entitlements.sku_id') ->get() ->pluck('title') ->sort() ->unique() ->values() ->all(); $hasBeta = in_array('beta', $skus); $result = [ 'skus' => $skus, 'enableBeta' => in_array('beta', $skus), // TODO: This will change when we enable all users to create domains 'enableDomains' => $isController && $hasCustomDomain, // TODO: Make 'enableDistlists' working for wallet controllers that aren't account owners 'enableDistlists' => $isController && $hasCustomDomain && $hasBeta, 'enableFiles' => !$isDegraded && $hasBeta && \config('app.with_files'), // TODO: Make 'enableFolders' working for wallet controllers that aren't account owners 'enableFolders' => $isController && $hasCustomDomain && $hasBeta, // TODO: Make 'enableResources' working for wallet controllers that aren't account owners 'enableResources' => $isController && $hasCustomDomain && $hasBeta, 'enableRooms' => $hasMeet, 'enableSettings' => $isController, 'enableUsers' => $isController, 'enableWallets' => $isController, 'enableCompanionapps' => $hasBeta, ]; return array_merge($process, $result); } /** * Create a new user record. * * @param \Illuminate\Http\Request $request The API request. * * @return \Illuminate\Http\JsonResponse The response */ public function store(Request $request) { $current_user = $this->guard()->user(); $owner = $current_user->walletOwner(); if ($owner->id != $current_user->id) { return $this->errorResponse(403); } $this->deleteBeforeCreate = null; if ($error_response = $this->validateUserRequest($request, null, $settings)) { return $error_response; } if (empty($request->package) || !($package = \App\Package::withEnvTenantContext()->find($request->package))) { $errors = ['package' => \trans('validation.packagerequired')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } if ($package->isDomain()) { $errors = ['package' => \trans('validation.packageinvalid')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } DB::beginTransaction(); // @phpstan-ignore-next-line if ($this->deleteBeforeCreate) { $this->deleteBeforeCreate->forceDelete(); } // Create user record $user = User::create([ 'email' => $request->email, 'password' => $request->password, ]); $this->activatePassCode($user); $owner->assignPackage($package, $user); if (!empty($settings)) { $user->setSettings($settings); } if (!empty($request->aliases)) { $user->setAliases($request->aliases); } DB::commit(); return response()->json([ 'status' => 'success', 'message' => \trans('app.user-create-success'), ]); } /** * Update user data. * * @param \Illuminate\Http\Request $request The API request. * @param string $id User identifier * * @return \Illuminate\Http\JsonResponse The response */ public function update(Request $request, $id) { $user = User::withEnvTenantContext()->find($id); if (empty($user)) { return $this->errorResponse(404); } $current_user = $this->guard()->user(); // TODO: Decide what attributes a user can change on his own profile if (!$current_user->canUpdate($user)) { return $this->errorResponse(403); } if ($error_response = $this->validateUserRequest($request, $user, $settings)) { return $error_response; } // Entitlements, only controller can do that if ($request->skus !== null && !$current_user->canDelete($user)) { return $this->errorResponse(422, "You have no permission to change entitlements"); } DB::beginTransaction(); SkusController::updateEntitlements($user, $request->skus); if (!empty($settings)) { $user->setSettings($settings); } if (!empty($request->password)) { $user->password = $request->password; $user->save(); } $this->activatePassCode($user); if (isset($request->aliases)) { $user->setAliases($request->aliases); } DB::commit(); $response = [ 'status' => 'success', 'message' => \trans('app.user-update-success'), ]; // For self-update refresh the statusInfo in the UI if ($user->id == $current_user->id) { $response['statusInfo'] = self::statusInfo($user); } return response()->json($response); } /** * Create a response data array for specified user. * * @param \App\User $user User object * * @return array Response data */ public static function userResponse(User $user): array { $response = array_merge($user->toArray(), self::objectState($user)); // Settings $response['settings'] = []; foreach ($user->settings()->whereIn('key', self::USER_SETTINGS)->get() as $item) { $response['settings'][$item->key] = $item->value; } // Status info $response['statusInfo'] = self::statusInfo($user); // Add more info to the wallet object output $map_func = function ($wallet) use ($user) { $result = $wallet->toArray(); if ($wallet->discount) { $result['discount'] = $wallet->discount->discount; $result['discount_description'] = $wallet->discount->description; } if ($wallet->user_id != $user->id) { $result['user_email'] = $wallet->owner->email; } $provider = \App\Providers\PaymentProvider::factory($wallet); $result['provider'] = $provider->name(); return $result; }; // Information about wallets and accounts for access checks $response['wallets'] = $user->wallets->map($map_func)->toArray(); $response['accounts'] = $user->accounts->map($map_func)->toArray(); $response['wallet'] = $map_func($user->wallet()); return $response; } /** * Prepare user statuses for the UI * * @param \App\User $user User object * * @return array Statuses array */ protected static function objectState($user): array { $state = parent::objectState($user); $state['isAccountDegraded'] = $user->isDegraded(true); return $state; } /** * Validate user input * * @param \Illuminate\Http\Request $request The API request. * @param \App\User|null $user User identifier * @param array $settings User settings (from the request) * * @return \Illuminate\Http\JsonResponse|null The error response on error */ protected function validateUserRequest(Request $request, $user, &$settings = []) { $rules = [ 'external_email' => 'nullable|email', 'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/', 'first_name' => 'string|nullable|max:128', 'last_name' => 'string|nullable|max:128', 'organization' => 'string|nullable|max:512', 'billing_address' => 'string|nullable|max:1024', 'country' => 'string|nullable|alpha|size:2', 'currency' => 'string|nullable|alpha|size:3', 'aliases' => 'array|nullable', ]; $controller = ($user ?: $this->guard()->user())->walletOwner(); // Handle generated password reset code if ($code = $request->input('passwordLinkCode')) { // Accept - input if (strpos($code, '-')) { $code = explode('-', $code)[1]; } $this->passCode = $this->guard()->user()->verificationcodes() ->where('code', $code)->where('active', false)->first(); // Generate a password for a new user with password reset link // FIXME: Should/can we have a user with no password set? if ($this->passCode && empty($user)) { $request->password = $request->password_confirmation = Str::random(16); $ignorePassword = true; } } if (empty($user) || !empty($request->password) || !empty($request->password_confirmation)) { if (empty($ignorePassword)) { $rules['password'] = ['required', 'confirmed', new Password($controller)]; } } $errors = []; // Validate input $v = Validator::make($request->all(), $rules); if ($v->fails()) { $errors = $v->errors()->toArray(); } // For new user validate email address if (empty($user)) { $email = $request->email; if (empty($email)) { $errors['email'] = \trans('validation.required', ['attribute' => 'email']); } elseif ($error = self::validateEmail($email, $controller, $this->deleteBeforeCreate)) { $errors['email'] = $error; } } // Validate aliases input if (isset($request->aliases)) { $aliases = []; $existing_aliases = $user ? $user->aliases()->get()->pluck('alias')->toArray() : []; foreach ($request->aliases as $idx => $alias) { if (is_string($alias) && !empty($alias)) { // Alias cannot be the same as the email address (new user) if (!empty($email) && Str::lower($alias) == Str::lower($email)) { continue; } // validate new aliases if ( !in_array($alias, $existing_aliases) && ($error = self::validateAlias($alias, $controller)) ) { if (!isset($errors['aliases'])) { $errors['aliases'] = []; } $errors['aliases'][$idx] = $error; continue; } $aliases[] = $alias; } } $request->aliases = $aliases; } if (!empty($errors)) { return response()->json(['status' => 'error', 'errors' => $errors], 422); } // Update user settings $settings = $request->only(array_keys($rules)); unset($settings['password'], $settings['aliases'], $settings['email']); return null; } /** * Execute (synchronously) specified step in a user setup process. * * @param \App\User $user User object * @param string $step Step identifier (as in self::statusInfo()) * * @return bool|null True if the execution succeeded, False if not, Null when * the job has been sent to the worker (result unknown) */ public static function execProcessStep(User $user, string $step): ?bool { try { if (strpos($step, 'domain-') === 0) { - list ($local, $domain) = explode('@', $user->email); - $domain = Domain::where('namespace', $domain)->first(); - - return DomainsController::execProcessStep($domain, $step); + return DomainsController::execProcessStep($user->domain(), $step); } switch ($step) { case 'user-ldap-ready': case 'user-imap-ready': // Use worker to do the job, frontend might not have the IMAP admin credentials \App\Jobs\User\CreateJob::dispatch($user->id); return null; } } catch (\Exception $e) { \Log::error($e); } return false; } /** * Email address validation for use as a user mailbox (login). * * @param string $email Email address * @param \App\User $user The account owner * @param null|\App\User|\App\Group $deleted Filled with an instance of a deleted user or group * with the specified email address, if exists * * @return ?string Error message on validation error */ public static function validateEmail(string $email, \App\User $user, &$deleted = null): ?string { $deleted = null; if (strpos($email, '@') === false) { return \trans('validation.entryinvalid', ['attribute' => 'email']); } list($login, $domain) = explode('@', Str::lower($email)); if (strlen($login) === 0 || strlen($domain) === 0) { return \trans('validation.entryinvalid', ['attribute' => 'email']); } // Check if domain exists $domain = Domain::withObjectTenantContext($user)->where('namespace', $domain)->first(); if (empty($domain)) { return \trans('validation.domaininvalid'); } // Validate login part alone $v = Validator::make( ['email' => $login], ['email' => ['required', new UserEmailLocal(!$domain->isPublic())]] ); if ($v->fails()) { return $v->errors()->toArray()['email'][0]; } // Check if it is one of domains available to the user if (!$domain->isPublic() && $user->id != $domain->walletOwner()->id) { return \trans('validation.entryexists', ['attribute' => 'domain']); } // Check if a user/group/resource/shared folder with specified address already exists if ( ($existing = User::emailExists($email, true)) || ($existing = \App\Group::emailExists($email, true)) || ($existing = \App\Resource::emailExists($email, true)) || ($existing = \App\SharedFolder::emailExists($email, true)) ) { // If this is a deleted user/group/resource/folder in the same custom domain // we'll force delete it before creating the target user if (!$domain->isPublic() && $existing->trashed()) { $deleted = $existing; } else { return \trans('validation.entryexists', ['attribute' => 'email']); } } // Check if an alias with specified address already exists. if (User::aliasExists($email) || \App\SharedFolder::aliasExists($email)) { return \trans('validation.entryexists', ['attribute' => 'email']); } return null; } /** * Email address validation for use as an alias. * * @param string $email Email address * @param \App\User $user The account owner * * @return ?string Error message on validation error */ public static function validateAlias(string $email, \App\User $user): ?string { if (strpos($email, '@') === false) { return \trans('validation.entryinvalid', ['attribute' => 'alias']); } list($login, $domain) = explode('@', Str::lower($email)); if (strlen($login) === 0 || strlen($domain) === 0) { return \trans('validation.entryinvalid', ['attribute' => 'alias']); } // Check if domain exists $domain = Domain::withObjectTenantContext($user)->where('namespace', $domain)->first(); if (empty($domain)) { return \trans('validation.domaininvalid'); } // Validate login part alone $v = Validator::make( ['alias' => $login], ['alias' => ['required', new UserEmailLocal(!$domain->isPublic())]] ); if ($v->fails()) { return $v->errors()->toArray()['alias'][0]; } // Check if it is one of domains available to the user if (!$domain->isPublic() && $user->id != $domain->walletOwner()->id) { return \trans('validation.entryexists', ['attribute' => 'domain']); } // Check if a user with specified address already exists if ($existing_user = User::emailExists($email, true)) { // Allow an alias in a custom domain to an address that was a user before if ($domain->isPublic() || !$existing_user->trashed()) { return \trans('validation.entryexists', ['attribute' => 'alias']); } } // Check if a group/resource/shared folder with specified address already exists if ( \App\Group::emailExists($email) || \App\Resource::emailExists($email) || \App\SharedFolder::emailExists($email) ) { return \trans('validation.entryexists', ['attribute' => 'alias']); } // Check if an alias with specified address already exists if (User::aliasExists($email) || \App\SharedFolder::aliasExists($email)) { // Allow assigning the same alias to a user in the same group account, // but only for non-public domains if ($domain->isPublic()) { return \trans('validation.entryexists', ['attribute' => 'alias']); } } return null; } /** * Activate password reset code (if set), and assign it to a user. * * @param \App\User $user The user */ protected function activatePassCode(User $user): void { // Activate the password reset code if ($this->passCode) { $this->passCode->user_id = $user->id; $this->passCode->active = true; $this->passCode->save(); } } } diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php index 76374a38..3e1a0c48 100644 --- a/src/app/Http/Controllers/RelationController.php +++ b/src/app/Http/Controllers/RelationController.php @@ -1,419 +1,409 @@ model::find($id); if (!$this->checkTenant($resource)) { return $this->errorResponse(404); } if (!$this->guard()->user()->canDelete($resource)) { return $this->errorResponse(403); } $resource->delete(); return response()->json([ 'status' => 'success', 'message' => \trans("app.{$this->label}-delete-success"), ]); } /** * Listing of resources belonging to the authenticated user. * * The resource entitlements billed to the current user wallet(s) * * @return \Illuminate\Http\JsonResponse */ public function index() { $user = $this->guard()->user(); $method = Str::plural(\lcfirst(\class_basename($this->model))); $query = call_user_func_array([$user, $method], $this->relationArgs); if (!empty($this->order)) { foreach ($this->order as $col) { $query->orderBy($col); } } // TODO: Search and paging $result = $query->get() ->map(function ($resource) { return $this->objectToClient($resource); }); $result = [ 'list' => $result, 'count' => count($result), 'hasMore' => false, 'message' => \trans("app.search-foundx{$this->label}s", ['x' => count($result)]), ]; return response()->json($result); } /** * Prepare resource statuses for the UI * * @param object $resource Resource object * * @return array Statuses array */ protected static function objectState($resource): array { $state = []; $reflect = new \ReflectionClass(get_class($resource)); foreach (array_keys($reflect->getConstants()) as $const) { if (strpos($const, 'STATUS_') === 0 && $const != 'STATUS_NEW') { $method = Str::camel('is_' . strtolower(substr($const, 7))); $state[$method] = $resource->{$method}(); } } - $with_imap = \config('app.with_imap'); $with_ldap = \config('app.with_ldap'); - $state['isReady'] = (!$with_imap || !isset($state['isImapReady']) || $state['isImapReady']) + $state['isReady'] = (!isset($state['isImapReady']) || $state['isImapReady']) && (!$with_ldap || !isset($state['isLdapReady']) || $state['isLdapReady']) && (!isset($state['isVerified']) || $state['isVerified']) && (!isset($state['isConfirmed']) || $state['isConfirmed']); - if (!$with_imap) { - unset($state['isImapReady']); - } if (!$with_ldap) { unset($state['isLdapReady']); } if (empty($state['isDeleted']) && method_exists($resource, 'trashed')) { $state['isDeleted'] = $resource->trashed(); } return $state; } /** * Prepare a resource object for the UI. * * @param object $object An object * @param bool $full Include all object properties * * @return array Object information */ protected function objectToClient($object, bool $full = false): array { if ($full) { $result = $object->toArray(); unset($result['tenant_id']); } else { $result = ['id' => $object->id]; foreach ($this->objectProps as $prop) { $result[$prop] = $object->{$prop}; } } $result = array_merge($result, $this->objectState($object)); return $result; } /** * Object status' process information. * * @param object $object The object to process * @param array $steps The steps definition * * @return array Process state information */ protected static function processStateInfo($object, array $steps): array { $process = []; $withLdap = \config('app.with_ldap'); - $withImap = \config('app.with_imap'); // Create a process check list foreach ($steps as $step_name => $state) { // Remove LDAP related steps if the backend is disabled if (!$withLdap && strpos($step_name, '-ldap-')) { continue; } - // Remove IMAP related steps if the backend is disabled - if (!$withImap && strpos($step_name, '-imap-')) { - continue; - } - $step = [ 'label' => $step_name, 'title' => \trans("app.process-{$step_name}"), ]; if (is_array($state)) { $step['link'] = $state[1]; $state = $state[0]; } $step['state'] = $state; $process[] = $step; } // Add domain specific steps if (method_exists($object, 'domain')) { $domain = $object->domain(); // If that is not a public domain if ($domain && !$domain->isPublic()) { $domain_status = API\V4\DomainsController::statusInfo($domain); $process = array_merge($process, $domain_status['process']); } } $all = count($process); $checked = count(array_filter($process, function ($v) { return $v['state']; })); $state = $all === $checked ? 'done' : 'running'; // After 180 seconds assume the process is in failed state, // this should unlock the Refresh button in the UI if ($all !== $checked && $object->created_at->diffInSeconds(\Carbon\Carbon::now()) > 180) { $state = 'failed'; } return [ 'process' => $process, 'processState' => $state, - 'isReady' => $all === $checked, + 'isDone' => $all === $checked, ]; } /** * Object status' process information update. * * @param object $object The object to process * * @return array Process state information */ protected function processStateUpdate($object): array { $response = $this->statusInfo($object); if (!empty(request()->input('refresh'))) { $updated = false; $async = false; $last_step = 'none'; foreach ($response['process'] as $idx => $step) { $last_step = $step['label']; if (!$step['state']) { $exec = $this->execProcessStep($object, $step['label']); // @phpstan-ignore-line if (!$exec) { if ($exec === null) { $async = true; } break; } $updated = true; } } if ($updated) { $response = $this->statusInfo($object); } - $success = $response['isReady']; + $success = $response['isDone']; $suffix = $success ? 'success' : 'error-' . $last_step; $response['status'] = $success ? 'success' : 'error'; $response['message'] = \trans('app.process-' . $suffix); if ($async && !$success) { $response['processState'] = 'waiting'; $response['status'] = 'success'; $response['message'] = \trans('app.process-async'); } } return $response; } /** * Set the resource configuration. * * @param int $id Resource identifier * * @return \Illuminate\Http\JsonResponse|void */ public function setConfig($id) { $resource = $this->model::find($id); if (!method_exists($this->model, 'setConfig')) { return $this->errorResponse(404); } if (!$this->checkTenant($resource)) { return $this->errorResponse(404); } if (!$this->guard()->user()->canUpdate($resource)) { return $this->errorResponse(403); } $errors = $resource->setConfig(request()->input()); if (!empty($errors)) { return response()->json(['status' => 'error', 'errors' => $errors], 422); } return response()->json([ 'status' => 'success', 'message' => \trans("app.{$this->label}-setconfig-success"), ]); } /** * Display information of a resource specified by $id. * * @param string $id The resource to show information for. * * @return \Illuminate\Http\JsonResponse */ public function show($id) { $resource = $this->model::find($id); if (!$this->checkTenant($resource)) { return $this->errorResponse(404); } if (!$this->guard()->user()->canRead($resource)) { return $this->errorResponse(403); } $response = $this->objectToClient($resource, true); if (!empty($statusInfo = $this->statusInfo($resource))) { $response['statusInfo'] = $statusInfo; } // Resource configuration, e.g. sender_policy, invitation_policy, acl if (method_exists($resource, 'getConfig')) { $response['config'] = $resource->getConfig(); } if (method_exists($resource, 'aliases')) { $response['aliases'] = $resource->aliases()->pluck('alias')->all(); } // Entitlements/Wallet info if (method_exists($resource, 'wallet')) { API\V4\SkusController::objectEntitlements($resource, $response); } return response()->json($response); } /** * Get a list of SKUs available to the resource. * * @param int $id Resource identifier * * @return \Illuminate\Http\JsonResponse */ public function skus($id) { $resource = $this->model::find($id); if (!$this->checkTenant($resource)) { return $this->errorResponse(404); } if (!$this->guard()->user()->canRead($resource)) { return $this->errorResponse(403); } return API\V4\SkusController::objectSkus($resource); } /** * Fetch resource status (and reload setup process) * * @param int $id Resource identifier * * @return \Illuminate\Http\JsonResponse */ public function status($id) { $resource = $this->model::find($id); if (!$this->checkTenant($resource)) { return $this->errorResponse(404); } if (!$this->guard()->user()->canRead($resource)) { return $this->errorResponse(403); } $response = $this->processStateUpdate($resource); $response = array_merge($response, $this->objectState($resource)); return response()->json($response); } /** * Resource status (extended) information * * @param object $resource Resource object * * @return array Status information */ public static function statusInfo($resource): array { return []; } } diff --git a/src/app/Jobs/Resource/CreateJob.php b/src/app/Jobs/Resource/CreateJob.php index 92e0fcd1..b51b7079 100644 --- a/src/app/Jobs/Resource/CreateJob.php +++ b/src/app/Jobs/Resource/CreateJob.php @@ -1,71 +1,80 @@ getResource(); if (!$resource) { return; } // sanity checks if ($resource->isDeleted()) { $this->fail(new \Exception("Resource {$this->resourceId} is marked as deleted.")); return; } if ($resource->trashed()) { $this->fail(new \Exception("Resource {$this->resourceId} is actually deleted.")); return; } $withLdap = \config('app.with_ldap'); // see if the domain is ready $domain = $resource->domain(); if (!$domain) { $this->fail(new \Exception("The domain for resource {$this->resourceId} does not exist.")); return; } if ($domain->isDeleted()) { $this->fail(new \Exception("The domain for resource {$this->resourceId} is marked as deleted.")); return; } if ($withLdap && !$domain->isLdapReady()) { $this->release(60); return; } if ($withLdap && !$resource->isLdapReady()) { \App\Backends\LDAP::createResource($resource); $resource->status |= \App\Resource::STATUS_LDAP_READY; $resource->save(); } - if (\config('app.with_imap') && !$resource->isImapReady()) { - if (!\App\Backends\IMAP::createResource($resource)) { - throw new \Exception("Failed to create mailbox for resource {$this->resourceId}."); + if (!$resource->isImapReady()) { + if (\config('app.with_imap')) { + if (!\App\Backends\IMAP::createResource($resource)) { + throw new \Exception("Failed to create mailbox for resource {$this->resourceId}."); + } + } else { + $folder = $resource->getSetting('folder'); + + if ($folder && !\App\Backends\IMAP::verifySharedFolder($folder)) { + $this->release(15); + return; + } } $resource->status |= \App\Resource::STATUS_IMAP_READY; } $resource->status |= \App\Resource::STATUS_ACTIVE; $resource->save(); } } diff --git a/src/app/Jobs/Resource/DeleteJob.php b/src/app/Jobs/Resource/DeleteJob.php index 9ffe9bbf..09543c1b 100644 --- a/src/app/Jobs/Resource/DeleteJob.php +++ b/src/app/Jobs/Resource/DeleteJob.php @@ -1,46 +1,48 @@ getResource(); if (!$resource) { return; } // sanity checks if ($resource->isDeleted()) { $this->fail(new \Exception("Resource {$this->resourceId} is already marked as deleted.")); return; } if (\config('app.with_ldap') && $resource->isLdapReady()) { \App\Backends\LDAP::deleteResource($resource); $resource->status ^= \App\Resource::STATUS_LDAP_READY; $resource->save(); } - if (\config('app.with_imap') && $resource->isImapReady()) { - if (!\App\Backends\IMAP::deleteResource($resource)) { - throw new \Exception("Failed to delete mailbox for resource {$this->resourceId}."); + if ($resource->isImapReady()) { + if (\config('app.with_imap')) { + if (!\App\Backends\IMAP::deleteResource($resource)) { + throw new \Exception("Failed to delete mailbox for resource {$this->resourceId}."); + } } $resource->status ^= \App\Resource::STATUS_IMAP_READY; } $resource->status |= \App\Resource::STATUS_DELETED; $resource->save(); } } diff --git a/src/app/Jobs/SharedFolder/CreateJob.php b/src/app/Jobs/SharedFolder/CreateJob.php index 0c44cb64..5b9c3320 100644 --- a/src/app/Jobs/SharedFolder/CreateJob.php +++ b/src/app/Jobs/SharedFolder/CreateJob.php @@ -1,72 +1,80 @@ getSharedFolder(); if (!$folder) { return; } // sanity checks if ($folder->isDeleted()) { $this->fail(new \Exception("Shared folder {$this->folderId} is marked as deleted.")); return; } if ($folder->trashed()) { $this->fail(new \Exception("Shared folder {$this->folderId} is actually deleted.")); return; } $withLdap = \config('app.with_ldap'); - $withImap = \config('app.with_imap'); // see if the domain is ready $domain = $folder->domain(); if (!$domain) { $this->fail(new \Exception("The domain for shared folder {$this->folderId} does not exist.")); return; } if ($domain->isDeleted()) { $this->fail(new \Exception("The domain for shared folder {$this->folderId} is marked as deleted.")); return; } if ($withLdap && !$domain->isLdapReady()) { $this->release(60); return; } if ($withLdap && !$folder->isLdapReady()) { \App\Backends\LDAP::createSharedFolder($folder); $folder->status |= \App\SharedFolder::STATUS_LDAP_READY; $folder->save(); } - if ($withImap && !$folder->isImapReady()) { - if (!\App\Backends\IMAP::createSharedFolder($folder)) { - throw new \Exception("Failed to create mailbox for shared folder {$this->folderId}."); + if (!$folder->isImapReady()) { + if (\config('app.with_imap')) { + if (!\App\Backends\IMAP::createSharedFolder($folder)) { + throw new \Exception("Failed to create mailbox for shared folder {$this->folderId}."); + } + } else { + $folderName = $folder->getSetting('folder'); + + if ($folderName && !\App\Backends\IMAP::verifySharedFolder($folderName)) { + $this->release(15); + return; + } } $folder->status |= \App\SharedFolder::STATUS_IMAP_READY; } $folder->status |= \App\SharedFolder::STATUS_ACTIVE; $folder->save(); } } diff --git a/src/app/Jobs/SharedFolder/DeleteJob.php b/src/app/Jobs/SharedFolder/DeleteJob.php index 8e4d5444..ef570888 100644 --- a/src/app/Jobs/SharedFolder/DeleteJob.php +++ b/src/app/Jobs/SharedFolder/DeleteJob.php @@ -1,47 +1,49 @@ getSharedFolder(); if (!$folder) { return; } // sanity checks if ($folder->isDeleted()) { $this->fail(new \Exception("Shared folder {$this->folderId} is already marked as deleted.")); return; } if (\config('app.with_ldap') && $folder->isLdapReady()) { \App\Backends\LDAP::deleteSharedFolder($folder); $folder->status ^= \App\SharedFolder::STATUS_LDAP_READY; // Already save in case of exception below $folder->save(); } if ($folder->isImapReady()) { - if (!\App\Backends\IMAP::deleteSharedFolder($folder)) { - throw new \Exception("Failed to delete mailbox for shared folder {$this->folderId}."); + if (\config('app.with_imap')) { + if (!\App\Backends\IMAP::deleteSharedFolder($folder)) { + throw new \Exception("Failed to delete mailbox for shared folder {$this->folderId}."); + } } $folder->status ^= \App\SharedFolder::STATUS_IMAP_READY; } $folder->status |= \App\SharedFolder::STATUS_DELETED; $folder->save(); } } diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php index f97117f6..4922cd38 100644 --- a/src/app/Jobs/User/CreateJob.php +++ b/src/app/Jobs/User/CreateJob.php @@ -1,107 +1,108 @@ isDeleted()`), or * * the user is actually deleted (`$user->deleted_at`), or * * the user is already marked as ready in LDAP (`$user->isLdapReady()`). * */ class CreateJob extends UserJob { /** * Execute the job. * * @return void * * @throws \Exception */ public function handle() { $user = $this->getUser(); if (!$user) { return; } if ($user->role) { // Admins/resellers don't reside in LDAP (for now) return; } if ($user->email == \config('imap.admin_login')) { // Ignore Cyrus admin account return; } // sanity checks if ($user->isDeleted()) { $this->fail(new \Exception("User {$this->userId} is marked as deleted.")); return; } if ($user->trashed()) { $this->fail(new \Exception("User {$this->userId} is actually deleted.")); return; } $withLdap = \config('app.with_ldap'); // see if the domain is ready $domain = $user->domain(); if (!$domain) { $this->fail(new \Exception("The domain for {$this->userId} does not exist.")); return; } if ($domain->isDeleted()) { $this->fail(new \Exception("The domain for {$this->userId} is marked as deleted.")); return; } if ($withLdap && !$domain->isLdapReady()) { $this->release(60); return; } - if (\config('abuse.suspend_enabled')) { + if (\config('abuse.suspend_enabled') && !$user->isSuspended()) { $code = \Artisan::call("user:abuse-check {$this->userId}"); if ($code == 2) { \Log::info("Suspending user due to suspected abuse: {$this->userId} {$user->email}"); $user->status |= \App\User::STATUS_SUSPENDED; } } if ($withLdap && !$user->isLdapReady()) { \App\Backends\LDAP::createUser($user); $user->status |= \App\User::STATUS_LDAP_READY; $user->save(); } if (!$user->isImapReady()) { if (\config('app.with_imap')) { if (!\App\Backends\IMAP::createUser($user)) { throw new \Exception("Failed to create mailbox for user {$this->userId}."); } } else { if (!\App\Backends\IMAP::verifyAccount($user->email)) { - throw new \Exception("Failed to find the mailbox for user {$this->userId}."); + $this->release(15); + return; } } $user->status |= \App\User::STATUS_IMAP_READY; } $user->status |= \App\User::STATUS_ACTIVE; $user->save(); } } diff --git a/src/app/Observers/SignupCodeObserver.php b/src/app/Observers/SignupCodeObserver.php index 96c0fb8a..23c3900b 100644 --- a/src/app/Observers/SignupCodeObserver.php +++ b/src/app/Observers/SignupCodeObserver.php @@ -1,77 +1,77 @@ code)) { $code->short_code = SignupCode::generateShortCode(); // FIXME: Replace this with something race-condition free while (true) { $code->code = Str::random($code_length); if (!SignupCode::find($code->code)) { break; } } } $code->headers = collect(request()->headers->all()) ->filter(function ($value, $key) { // remove some headers we don't care about return !in_array($key, ['cookie', 'referer', 'x-test-payment-provider', 'origin']); }) ->map(function ($value) { return is_array($value) && count($value) == 1 ? $value[0] : $value; }) ->all(); $code->expires_at = Carbon::now()->addHours($exp_hours); $code->ip_address = request()->ip(); if ($code->email && strpos($code->email, '@')) { $parts = explode('@', $code->email); $code->local_part = $parts[0]; $code->domain_part = $parts[1]; } } /** * Handle the "updating" event. * * @param SignupCode $code The code being updated. * * @return void */ public function updating(SignupCode $code) { - if ($code->email) { + if ($code->email && strpos($code->email, '@')) { $parts = explode('@', $code->email); $code->local_part = $parts[0]; $code->domain_part = $parts[1]; } else { $code->local_part = null; $code->domain_part = null; } } } diff --git a/src/app/Traits/EmailPropertyTrait.php b/src/app/Traits/EmailPropertyTrait.php index 640f2ed4..00416eac 100644 --- a/src/app/Traits/EmailPropertyTrait.php +++ b/src/app/Traits/EmailPropertyTrait.php @@ -1,92 +1,94 @@ email) && defined('static::EMAIL_TEMPLATE')) { $template = static::EMAIL_TEMPLATE; // @phpstan-ignore-line $defaults = [ 'type' => 'mail', ]; foreach (['id', 'domainName', 'type'] as $prop) { if (strpos($template, "{{$prop}}") === false) { continue; } $value = $model->{$prop} ?? ($defaults[$prop] ?? ''); if ($value === '' || $value === null) { throw new \Exception("Missing '{$prop}' property for " . static::class); } $template = str_replace("{{$prop}}", $value, $template); } $model->email = strtolower($template); } }); } /** * Returns the object's domain (including soft-deleted). * * @return ?\App\Domain The domain to which the object belongs to, NULL if it does not exist */ public function domain(): ?\App\Domain { if (empty($this->email) && isset($this->domainName)) { $domainName = $this->domainName; - } else { + } else if (strpos($this->email, '@')) { list($local, $domainName) = explode('@', $this->email); + } else { + return null; } return \App\Domain::withTrashed()->where('namespace', $domainName)->first(); } /** * Find whether an email address exists as a model object (including soft-deleted). * * @param string $email Email address * @param bool $return_object Return model instance instead of a boolean * * @return static|bool True or Model object if found, False otherwise */ public static function emailExists(string $email, bool $return_object = false) { if (strpos($email, '@') === false) { return false; } $email = \strtolower($email); $object = static::withTrashed()->where('email', $email)->first(); if ($object) { return $return_object ? $object : true; } return false; } /** * Ensure the email is appropriately cased. * * @param string $email Email address */ public function setEmailAttribute(string $email): void { $this->attributes['email'] = strtolower($email); } } diff --git a/src/resources/vue/Widgets/Status.vue b/src/resources/vue/Widgets/Status.vue index bebc7845..79688ea1 100644 --- a/src/resources/vue/Widgets/Status.vue +++ b/src/resources/vue/Widgets/Status.vue @@ -1,204 +1,204 @@ diff --git a/src/tests/Browser/StatusTest.php b/src/tests/Browser/StatusTest.php index 5897dc92..23e096a2 100644 --- a/src/tests/Browser/StatusTest.php +++ b/src/tests/Browser/StatusTest.php @@ -1,287 +1,287 @@ first(); if ($domain->isConfirmed()) { $domain->status ^= Domain::STATUS_CONFIRMED; $domain->save(); } $john = $this->getTestUser('john@kolab.org'); $john->created_at = Carbon::now(); if ($john->isImapReady()) { $john->status ^= User::STATUS_IMAP_READY; } $john->save(); $this->browse(function ($browser) use ($john, $domain) { $browser->visit(new Home()) ->submitLogon('john@kolab.org', 'simple123', true) ->on(new Dashboard()) ->with(new Status(), function ($browser) use ($john) { $browser->assertSeeIn('@body', 'We are preparing your account') ->assertProgress(71, 'Creating a mailbox...', 'pending') ->assertMissing('#status-verify') ->assertMissing('#status-link') ->assertMissing('@refresh-button') ->assertMissing('@refresh-text'); $john->status |= User::STATUS_IMAP_READY; $john->save(); // Wait for auto-refresh, expect domain-confirmed step $browser->pause(6000) ->assertSeeIn('@body', 'Your account is almost ready') ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed') ->assertMissing('@refresh-button') ->assertMissing('@refresh-text') ->assertMissing('#status-verify') ->assertVisible('#status-link'); }) // check if the link to domain info page works ->click('#status-link') ->on(new DomainInfo()) ->back() ->on(new Dashboard()) ->with(new Status(), function ($browser) { $browser->assertMissing('@refresh-button') ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed'); }); // Confirm the domain and wait until the whole status box disappears $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); // This should take less than 10 seconds $browser->waitUntilMissing('@status', 10); }); // Test the Refresh button if ($domain->isConfirmed()) { $domain->status ^= Domain::STATUS_CONFIRMED; $domain->save(); } $john->created_at = Carbon::now()->subSeconds(3600); if ($john->isImapReady()) { $john->status ^= User::STATUS_IMAP_READY; } $john->save(); $this->browse(function ($browser) use ($john, $domain) { $browser->visit(new Dashboard()) ->with(new Status(), function ($browser) use ($john, $domain) { $browser->assertSeeIn('@body', 'We are preparing your account') ->assertProgress(71, 'Creating a mailbox...', 'failed') ->assertVisible('@refresh-button') ->assertVisible('@refresh-text'); $browser->click('@refresh-button') ->assertToast(Toast::TYPE_SUCCESS, 'Setup process has been pushed. Please wait.'); $john->status |= User::STATUS_IMAP_READY; $john->save(); $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); }) ->waitUntilMissing('@status', 10); }); } /** * Test domain status on domains list and domain info page * * @depends testDashboard */ public function testDomainStatus(): void { $domain = Domain::where('namespace', 'kolab.org')->first(); $domain->created_at = Carbon::now(); $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; $domain->save(); // side-step $this->assertFalse($domain->isNew()); $this->assertTrue($domain->isActive()); $this->assertTrue($domain->isLdapReady()); $this->assertTrue($domain->isExternal()); $this->assertFalse($domain->isHosted()); $this->assertFalse($domain->isConfirmed()); $this->assertFalse($domain->isVerified()); $this->assertFalse($domain->isSuspended()); $this->assertFalse($domain->isDeleted()); $this->browse(function ($browser) use ($domain) { // Test auto-refresh $browser->on(new Dashboard()) ->click('@links a.link-domains') ->on(new DomainList()) ->waitFor('@table tbody tr') // Assert domain status icon ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-globe.text-danger') ->assertText('@table tbody tr:first-child td:first-child svg title', 'Not Ready') ->click('@table tbody tr:first-child td:first-child a') ->on(new DomainInfo()) ->with(new Status(), function ($browser) { $browser->assertSeeIn('@body', 'We are preparing the domain') ->assertProgress(50, 'Verifying a custom domain...', 'pending') ->assertMissing('@refresh-button') ->assertMissing('@refresh-text') ->assertMissing('#status-link') ->assertMissing('#status-verify'); }); $domain->status |= Domain::STATUS_VERIFIED; $domain->save(); // This should take less than 10 seconds $browser->waitFor('@status.process-failed') ->with(new Status(), function ($browser) { $browser->assertSeeIn('@body', 'The domain is almost ready') ->assertProgress(75, 'Verifying an ownership of a custom domain...', 'failed') ->assertMissing('@refresh-button') ->assertMissing('@refresh-text') ->assertMissing('#status-link') ->assertVisible('#status-verify'); }); $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); // Test Verify button $browser->click('@status #status-verify') ->assertToast(Toast::TYPE_SUCCESS, 'Domain verified successfully.') ->waitUntilMissing('@status') ->waitUntilMissing('@verify') ->assertVisible('@config'); }); } /** * Test user status on users list and user info page * * @depends testDashboard */ public function testUserStatus(): void { $john = $this->getTestUser('john@kolab.org'); $john->created_at = Carbon::now(); if ($john->isImapReady()) { $john->status ^= User::STATUS_IMAP_READY; } $john->save(); $domain = Domain::where('namespace', 'kolab.org')->first(); if ($domain->isConfirmed()) { $domain->status ^= Domain::STATUS_CONFIRMED; $domain->save(); } $this->browse(function ($browser) use ($john, $domain) { $browser->visit(new Dashboard()) ->click('@links a.link-users') ->on(new UserList()) ->waitFor('@table tbody tr') // Assert user status icons ->assertVisible('@table tbody tr:first-child td:first-child svg.fa-user.text-success') ->assertText('@table tbody tr:first-child td:first-child svg title', 'Active') ->assertVisible('@table tbody tr:nth-child(3) td:first-child svg.fa-user.text-danger') ->assertText('@table tbody tr:nth-child(3) td:first-child svg title', 'Not Ready') ->click('@table tbody tr:nth-child(3) td:first-child a') ->on(new UserInfo()) ->with('@form', function (Browser $browser) { // Assert state in the user edit form $browser->assertSeeIn('div.row:nth-child(1) label', 'Status') ->assertSeeIn('div.row:nth-child(1) #status', 'Not Ready'); }) ->with(new Status(), function ($browser) use ($john) { $browser->assertSeeIn('@body', 'We are preparing the user account') ->assertProgress(71, 'Creating a mailbox...', 'pending') ->assertMissing('#status-verify') ->assertMissing('#status-link') ->assertMissing('@refresh-button') ->assertMissing('@refresh-text'); $john->status |= User::STATUS_IMAP_READY; $john->save(); // Wait for auto-refresh, expect domain-confirmed step $browser->pause(6000) ->assertSeeIn('@body', 'The user account is almost ready') ->assertProgress(85, 'Verifying an ownership of a custom domain...', 'failed') ->assertMissing('@refresh-button') ->assertMissing('@refresh-text') ->assertMissing('#status-verify') ->assertVisible('#status-link'); }) ->assertSeeIn('#status', 'Active'); // Confirm the domain and wait until the whole status box disappears $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); // This should take less than 10 seconds $browser->waitUntilMissing('@status', 10); }); } } diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php index 1e417fb6..2ab062c0 100644 --- a/src/tests/Feature/Controller/DomainsTest.php +++ b/src/tests/Feature/Controller/DomainsTest.php @@ -1,618 +1,620 @@ deleteTestUser('test1@' . \config('app.domain')); $this->deleteTestUser('test2@' . \config('app.domain')); $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); Sku::where('title', 'test')->delete(); } public function tearDown(): void { $this->deleteTestUser('test1@' . \config('app.domain')); $this->deleteTestUser('test2@' . \config('app.domain')); $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); Sku::where('title', 'test')->delete(); $domain = $this->getTestDomain('kolab.org'); $domain->settings()->whereIn('key', ['spf_whitelist'])->delete(); parent::tearDown(); } /** * Test domain confirm request * @group skipci */ public function testConfirm(): void { Queue::fake(); $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $user = $this->getTestUser('test1@domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('error', $json['status']); $this->assertEquals('Domain ownership verification failed.', $json['message']); $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain verified successfully.', $json['message']); $this->assertTrue(is_array($json['statusInfo'])); // Not authorized access $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(403); // Authorized access by additional account controller $domain = $this->getTestDomain('kolab.org'); $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); } /** * Test domain delete request (DELETE /api/v4/domains/) */ public function testDestroy(): void { Queue::fake(); $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $john = $this->getTestUser('john@kolab.org'); $johns_domain = $this->getTestDomain('kolab.org'); $user1 = $this->getTestUser('test1@' . \config('app.domain')); $user2 = $this->getTestUser('test2@' . \config('app.domain')); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user1->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); // Not authorized access $response = $this->actingAs($john)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(403); // Can't delete non-empty domain $response = $this->actingAs($john)->delete("api/v4/domains/{$johns_domain->id}"); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('error', $json['status']); $this->assertEquals('Unable to delete a domain with assigned users or other objects.', $json['message']); // Successful deletion $response = $this->actingAs($user1)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain deleted successfully.', $json['message']); $this->assertTrue($domain->fresh()->trashed()); // Authorized access by additional account controller $this->deleteTestDomain('domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user1->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $user1->wallets()->first()->addController($user2); $response = $this->actingAs($user2)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain deleted successfully.', $json['message']); $this->assertTrue($domain->fresh()->trashed()); } /** * Test fetching domains list */ public function testIndex(): void { // User with no domains $user = $this->getTestUser('test1@domainscontroller.com'); $response = $this->actingAs($user)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("0 domains have been found.", $json['message']); $this->assertSame([], $json['list']); // User with custom domain(s) $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $response = $this->actingAs($john)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(1, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("1 domains have been found.", $json['message']); $this->assertCount(1, $json['list']); $this->assertSame('kolab.org', $json['list'][0]['namespace']); // Values below are tested by Unit tests $this->assertArrayHasKey('isConfirmed', $json['list'][0]); $this->assertArrayHasKey('isDeleted', $json['list'][0]); $this->assertArrayHasKey('isVerified', $json['list'][0]); $this->assertArrayHasKey('isSuspended', $json['list'][0]); $this->assertArrayHasKey('isActive', $json['list'][0]); if (\config('app.with_ldap')) { $this->assertArrayHasKey('isLdapReady', $json['list'][0]); } else { $this->assertArrayNotHasKey('isLdapReady', $json['list'][0]); } $this->assertArrayHasKey('isReady', $json['list'][0]); $response = $this->actingAs($ned)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertCount(1, $json['list']); $this->assertSame('kolab.org', $json['list'][0]['namespace']); } /** * Test domain config update (POST /api/v4/domains//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $domain = $this->getTestDomain('kolab.org'); $domain->setSetting('spf_whitelist', null); // Test unknown domain id $post = ['spf_whitelist' => []]; $response = $this->actingAs($john)->post("/api/v4/domains/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['spf_whitelist' => []]; $response = $this->actingAs($jack)->post("/api/v4/domains/{$domain->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['grey' => 1]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['grey']); $this->assertNull($domain->fresh()->getSetting('spf_whitelist')); // Test some valid data $post = ['spf_whitelist' => ['.test.domain.com']]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame('Domain settings updated successfully.', $json['message']); $expected = \json_encode($post['spf_whitelist']); $this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist')); // Test input validation $post = ['spf_whitelist' => ['aaa']]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame( 'The entry format is invalid. Expected a domain name starting with a dot.', $json['errors']['spf_whitelist'][0] ); $this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist')); } /** * Test fetching domain info */ public function testShow(): void { $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $user = $this->getTestUser('test1@domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); $discount = \App\Discount::withEnvTenantContext()->where('code', 'TEST')->first(); $wallet = $user->wallet(); $wallet->discount()->associate($discount); $wallet->save(); Entitlement::create([ 'wallet_id' => $user->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals($domain->id, $json['id']); $this->assertEquals($domain->namespace, $json['namespace']); $this->assertEquals($domain->status, $json['status']); $this->assertEquals($domain->type, $json['type']); $this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']); $this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']); $this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']); $this->assertSame([], $json['config']['spf_whitelist']); $this->assertCount(4, $json['mx']); $this->assertTrue(strpos(implode("\n", $json['mx']), $domain->namespace) !== false); $this->assertCount(8, $json['dns']); $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false); $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false); $this->assertTrue(is_array($json['statusInfo'])); // Values below are tested by Unit tests $this->assertArrayHasKey('isConfirmed', $json); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isVerified', $json); $this->assertArrayHasKey('isSuspended', $json); $this->assertArrayHasKey('isActive', $json); if (\config('app.with_ldap')) { $this->assertArrayHasKey('isLdapReady', $json); } else { $this->assertArrayNotHasKey('isLdapReady', $json); } $this->assertArrayHasKey('isReady', $json); $this->assertCount(1, $json['skus']); $this->assertSame(1, $json['skus'][$sku_domain->id]['count']); $this->assertSame([0], $json['skus'][$sku_domain->id]['costs']); $this->assertSame($wallet->id, $json['wallet']['id']); $this->assertSame($wallet->balance, $json['wallet']['balance']); $this->assertSame($wallet->currency, $json['wallet']['currency']); $this->assertSame($discount->discount, $json['wallet']['discount']); $this->assertSame($discount->description, $json['wallet']['discount_description']); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); // Not authorized - Other account domain $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(403); $domain = $this->getTestDomain('kolab.org'); // Ned is an additional controller on kolab.org's wallet $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(200); // Jack has no entitlement/control over kolab.org $response = $this->actingAs($jack)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(403); } /** * Test fetching SKUs list for a domain (GET /domains//skus) */ public function testSkus(): void { $user = $this->getTestUser('john@kolab.org'); $domain = $this->getTestDomain('kolab.org'); // Unauth access not allowed $response = $this->get("api/v4/domains/{$domain->id}/skus"); $response->assertStatus(401); // Create an sku for another tenant, to make sure it is not included in the result $nsku = Sku::create([ 'title' => 'test', 'name' => 'Test', 'description' => '', 'active' => true, 'cost' => 100, 'handler_class' => 'App\Handlers\Domain', ]); $tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first(); $nsku->tenant_id = $tenant->id; $nsku->save(); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/skus"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSkuElement('domain-hosting', $json[0], [ 'prio' => 0, 'type' => 'domain', 'handler' => 'DomainHosting', 'enabled' => true, 'readonly' => true, ]); } /** * Test fetching domain status (GET /api/v4/domains//status) * and forcing setup process update (?refresh=1) * * @group dns */ public function testStatus(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $domain = $this->getTestDomain('kolab.org'); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/domains/{$domain->id}/status"); $response->assertStatus(403); $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; $domain->save(); // Get domain status $response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse($json['isVerified']); $this->assertFalse($json['isReady']); + $this->assertFalse($json['isDone']); $this->assertCount(4, $json['process']); $this->assertSame('domain-verified', $json['process'][2]['label']); $this->assertSame(false, $json['process'][2]['state']); $this->assertTrue(empty($json['status'])); $this->assertTrue(empty($json['message'])); // Now "reboot" the process and verify the domain $response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isVerified']); $this->assertTrue($json['isReady']); + $this->assertTrue($json['isDone']); $this->assertCount(4, $json['process']); $this->assertSame('domain-verified', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('domain-confirmed', $json['process'][3]['label']); $this->assertSame(true, $json['process'][3]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); // TODO: Test completing all process steps } /** * Test domain creation (POST /api/v4/domains) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test empty request $response = $this->actingAs($john)->post("/api/v4/domains", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The namespace field is required.", $json['errors']['namespace'][0]); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['namespace']); $this->assertCount(2, $json); // Test access by user not being a wallet controller $post = ['namespace' => 'domainscontroller.com']; $response = $this->actingAs($jack)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['namespace' => '--']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified domain is invalid.', $json['errors']['namespace'][0]); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['namespace']); // Test an existing domain $post = ['namespace' => 'kolab.org']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified domain is not available.', $json['errors']['namespace']); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); // Missing package $post = ['namespace' => 'domainscontroller.com']; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Package is required.", $json['errors']['package']); $this->assertCount(2, $json); // Invalid package $post['package'] = $package_kolab->id; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Invalid package selected.", $json['errors']['package']); $this->assertCount(2, $json); // Test full and valid data $post['package'] = $package_domain->id; $response = $this->actingAs($john)->post("/api/v4/domains", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Domain created successfully.", $json['message']); $this->assertCount(2, $json); $domain = Domain::where('namespace', $post['namespace'])->first(); $this->assertInstanceOf(Domain::class, $domain); // Assert the new domain entitlements $this->assertEntitlements($domain, ['domain-hosting']); // Assert the wallet to which the new domain should be assigned to $wallet = $domain->wallet(); $this->assertSame($john->wallets->first()->id, $wallet->id); // Test re-creating a domain $domain->delete(); $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame("Domain created successfully.", $json['message']); $this->assertCount(2, $json); $domain = Domain::where('namespace', $post['namespace'])->first(); $this->assertInstanceOf(Domain::class, $domain); $this->assertEntitlements($domain, ['domain-hosting']); $wallet = $domain->wallet(); $this->assertSame($john->wallets->first()->id, $wallet->id); // Test creating a domain that is soft-deleted and belongs to another user $domain->delete(); $domain->entitlements()->withTrashed()->update(['wallet_id' => $jack->wallets->first()->id]); $response = $this->actingAs($john)->post("/api/v4/domains", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified domain is not available.', $json['errors']['namespace']); // Test acting as account controller (not owner) $this->markTestIncomplete(); } } diff --git a/src/tests/Feature/Controller/GroupsTest.php b/src/tests/Feature/Controller/GroupsTest.php index 4b410cc5..68696710 100644 --- a/src/tests/Feature/Controller/GroupsTest.php +++ b/src/tests/Feature/Controller/GroupsTest.php @@ -1,650 +1,650 @@ deleteTestGroup('group-test@kolab.org'); $this->deleteTestGroup('group-test2@kolab.org'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestGroup('group-test@kolab.org'); $this->deleteTestGroup('group-test2@kolab.org'); parent::tearDown(); } /** * Test group deleting (DELETE /api/v4/groups/) */ public function testDestroy(): void { // First create some groups to delete $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unauth access $response = $this->delete("api/v4/groups/{$group->id}"); $response->assertStatus(401); // Test non-existing group $response = $this->actingAs($john)->delete("api/v4/groups/abc"); $response->assertStatus(404); // Test access to other user's group $response = $this->actingAs($jack)->delete("api/v4/groups/{$group->id}"); $response->assertStatus(403); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test removing a group $response = $this->actingAs($john)->delete("api/v4/groups/{$group->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals("Distribution list deleted successfully.", $json['message']); } /** * Test groups listing (GET /api/v4/groups) */ public function testIndex(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unauth access $response = $this->get("api/v4/groups"); $response->assertStatus(401); // Test a user with no groups $response = $this->actingAs($jack)->get("/api/v4/groups"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("0 distribution lists have been found.", $json['message']); $this->assertSame([], $json['list']); // Test a user with a single group $response = $this->actingAs($john)->get("/api/v4/groups"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(1, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("1 distribution lists have been found.", $json['message']); $this->assertCount(1, $json['list']); $this->assertSame($group->id, $json['list'][0]['id']); $this->assertSame($group->email, $json['list'][0]['email']); $this->assertSame($group->name, $json['list'][0]['name']); $this->assertArrayHasKey('isDeleted', $json['list'][0]); $this->assertArrayHasKey('isSuspended', $json['list'][0]); $this->assertArrayHasKey('isActive', $json['list'][0]); $this->assertArrayHasKey('isLdapReady', $json['list'][0]); // Test that another wallet controller has access to groups $response = $this->actingAs($ned)->get("/api/v4/groups"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(1, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("1 distribution lists have been found.", $json['message']); $this->assertCount(1, $json['list']); $this->assertSame($group->email, $json['list'][0]['email']); } /** * Test group config update (POST /api/v4/groups//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unknown group id $post = ['sender_policy' => []]; $response = $this->actingAs($john)->post("/api/v4/groups/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['sender_policy' => []]; $response = $this->actingAs($jack)->post("/api/v4/groups/{$group->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['test' => 1]; $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']); $group->refresh(); $this->assertNull($group->getSetting('test')); $this->assertNull($group->getSetting('sender_policy')); // Test some valid data $post = ['sender_policy' => ['domain.com']]; $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame('Distribution list settings updated successfully.', $json['message']); $this->assertSame(['sender_policy' => $post['sender_policy']], $group->fresh()->getConfig()); // Test input validation $post = ['sender_policy' => [5]]; $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame( 'The entry format is invalid. Expected an email, domain, or part of it.', $json['errors']['sender_policy'][0] ); $this->assertSame(['sender_policy' => ['domain.com']], $group->fresh()->getConfig()); } /** * Test fetching group data/profile (GET /api/v4/groups/) */ public function testShow(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); $group->setSetting('sender_policy', '["test"]'); // Test unauthorized access to a profile of other user $response = $this->get("/api/v4/groups/{$group->id}"); $response->assertStatus(401); // Test unauthorized access to a group of another user $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}"); $response->assertStatus(403); // John: Group owner - non-existing group $response = $this->actingAs($john)->get("/api/v4/groups/abc"); $response->assertStatus(404); // John: Group owner $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($group->id, $json['id']); $this->assertSame($group->email, $json['email']); $this->assertSame($group->name, $json['name']); $this->assertSame($group->members, $json['members']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isSuspended', $json); $this->assertArrayHasKey('isActive', $json); $this->assertArrayHasKey('isLdapReady', $json); $this->assertSame(['sender_policy' => ['test']], $json['config']); $this->assertCount(1, $json['skus']); } /** * Test fetching SKUs list for a group (GET /groups//skus) */ public function testSkus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Unauth access not allowed $response = $this->get("api/v4/groups/{$group->id}/skus"); $response->assertStatus(401); // Unauthorized access not allowed $response = $this->actingAs($jack)->get("api/v4/groups/{$group->id}/skus"); $response->assertStatus(403); $response = $this->actingAs($john)->get("api/v4/groups/{$group->id}/skus"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSkuElement('group', $json[0], [ 'prio' => 0, 'type' => 'group', 'handler' => 'Group', 'enabled' => true, 'readonly' => true, ]); } /** * Test fetching group status (GET /api/v4/groups//status) * and forcing setup process update (?refresh=1) */ public function testStatus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unauthorized access $response = $this->get("/api/v4/groups/abc/status"); $response->assertStatus(401); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}/status"); $response->assertStatus(403); $group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE; $group->save(); // Get group status $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse($json['isLdapReady']); $this->assertFalse($json['isReady']); $this->assertFalse($json['isSuspended']); $this->assertTrue($json['isActive']); $this->assertFalse($json['isDeleted']); $this->assertCount(6, $json['process']); $this->assertSame('distlist-new', $json['process'][0]['label']); $this->assertSame(true, $json['process'][0]['state']); $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']); $this->assertSame(false, $json['process'][1]['state']); $this->assertTrue(empty($json['status'])); $this->assertTrue(empty($json['message'])); // Make sure the domain is confirmed (other test might unset that status) $domain = $this->getTestDomain('kolab.org'); $domain->status |= \App\Domain::STATUS_CONFIRMED; $domain->save(); // Now "reboot" the process and the group $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isLdapReady']); $this->assertTrue($json['isReady']); $this->assertCount(6, $json['process']); $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']); $this->assertSame(true, $json['process'][1]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); // Test a case when a domain is not ready $domain->status ^= \App\Domain::STATUS_CONFIRMED; $domain->save(); $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isLdapReady']); $this->assertTrue($json['isReady']); $this->assertCount(6, $json['process']); $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']); $this->assertSame(true, $json['process'][1]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); } /** * Test GroupsController::statusInfo() */ public function testStatusInfo(): void { $john = $this->getTestUser('john@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); $group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE; $group->save(); $result = GroupsController::statusInfo($group); - $this->assertFalse($result['isReady']); + $this->assertFalse($result['isDone']); $this->assertCount(6, $result['process']); $this->assertSame('distlist-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('distlist-ldap-ready', $result['process'][1]['label']); $this->assertSame(false, $result['process'][1]['state']); $this->assertSame('running', $result['processState']); $group->created_at = Carbon::now()->subSeconds(181); $group->save(); $result = GroupsController::statusInfo($group); $this->assertSame('failed', $result['processState']); $group->status |= Group::STATUS_LDAP_READY; $group->save(); $result = GroupsController::statusInfo($group); - $this->assertTrue($result['isReady']); + $this->assertTrue($result['isDone']); $this->assertCount(6, $result['process']); $this->assertSame('distlist-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('distlist-ldap-ready', $result['process'][1]['label']); $this->assertSame(true, $result['process'][2]['state']); $this->assertSame('done', $result['processState']); } /** * Test group creation (POST /api/v4/groups) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test unauth request $response = $this->post("/api/v4/groups", []); $response->assertStatus(401); // Test non-controller user $response = $this->actingAs($jack)->post("/api/v4/groups", []); $response->assertStatus(403); // Test empty request $response = $this->actingAs($john)->post("/api/v4/groups", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The email field is required.", $json['errors']['email']); $this->assertSame("At least one recipient is required.", $json['errors']['members']); $this->assertSame("The name field is required.", $json['errors']['name'][0]); $this->assertCount(2, $json); $this->assertCount(3, $json['errors']); // Test missing members and name $post = ['email' => 'group-test@kolab.org']; $response = $this->actingAs($john)->post("/api/v4/groups", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("At least one recipient is required.", $json['errors']['members']); $this->assertSame("The name field is required.", $json['errors']['name'][0]); $this->assertCount(2, $json); $this->assertCount(2, $json['errors']); // Test invalid email and too long name $post = ['email' => 'invalid', 'name' => str_repeat('A', 192)]; $response = $this->actingAs($john)->post("/api/v4/groups", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame("The specified email is invalid.", $json['errors']['email']); $this->assertSame("The name may not be greater than 191 characters.", $json['errors']['name'][0]); $this->assertCount(3, $json['errors']); // Test successful group creation $post = [ 'name' => 'Test Group', 'email' => 'group-test@kolab.org', 'members' => ['test1@domain.tld', 'test2@domain.tld'] ]; $response = $this->actingAs($john)->post("/api/v4/groups", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Distribution list created successfully.", $json['message']); $this->assertCount(2, $json); $group = Group::where('email', 'group-test@kolab.org')->first(); $this->assertInstanceOf(Group::class, $group); $this->assertSame($post['email'], $group->email); $this->assertSame($post['members'], $group->members); $this->assertTrue($john->groups()->get()->contains($group)); // Group name must be unique within a domain $post['email'] = 'group-test2@kolab.org'; $post['members'] = ['test1@domain.tld']; $response = $this->actingAs($john)->post("/api/v4/groups", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame("The specified name is not available.", $json['errors']['name'][0]); } /** * Test group update (PUT /api/v4/groups/) */ public function testUpdate(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unauthorized update $response = $this->get("/api/v4/groups/{$group->id}", []); $response->assertStatus(401); // Test unauthorized update $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}", []); $response->assertStatus(403); // Test updating - missing members $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("At least one recipient is required.", $json['errors']['members']); $this->assertCount(2, $json); // Test some invalid data $post = ['members' => ['test@domain.tld', 'invalid']]; $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified email address is invalid.', $json['errors']['members'][1]); // Valid data - members and name changed $post = [ 'name' => 'Test Gr', 'members' => ['member1@test.domain', 'member2@test.domain'] ]; $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Distribution list updated successfully.", $json['message']); $this->assertCount(2, $json); $group->refresh(); $this->assertSame($post['name'], $group->name); $this->assertSame($post['members'], $group->members); } /** * Group email address validation. */ public function testValidateGroupEmail(): void { $john = $this->getTestUser('john@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); // Invalid email $result = GroupsController::validateGroupEmail('', $john); $this->assertSame("The email field is required.", $result); $result = GroupsController::validateGroupEmail('kolab.org', $john); $this->assertSame("The specified email is invalid.", $result); $result = GroupsController::validateGroupEmail('.@kolab.org', $john); $this->assertSame("The specified email is invalid.", $result); $result = GroupsController::validateGroupEmail('test123456@localhost', $john); $this->assertSame("The specified domain is invalid.", $result); $result = GroupsController::validateGroupEmail('test123456@unknown-domain.org', $john); $this->assertSame("The specified domain is invalid.", $result); // forbidden public domain $result = GroupsController::validateGroupEmail('testtest@kolabnow.com', $john); $this->assertSame("The specified domain is not available.", $result); // existing alias $result = GroupsController::validateGroupEmail('jack.daniels@kolab.org', $john); $this->assertSame("The specified email is not available.", $result); // existing user $result = GroupsController::validateGroupEmail('ned@kolab.org', $john); $this->assertSame("The specified email is not available.", $result); // existing group $result = GroupsController::validateGroupEmail('group-test@kolab.org', $john); $this->assertSame("The specified email is not available.", $result); // valid $result = GroupsController::validateGroupEmail('admin@kolab.org', $john); $this->assertSame(null, $result); } /** * Group member email address validation. */ public function testValidateMemberEmail(): void { $john = $this->getTestUser('john@kolab.org'); // Invalid format $result = GroupsController::validateMemberEmail('kolab.org', $john); $this->assertSame("The specified email address is invalid.", $result); $result = GroupsController::validateMemberEmail('.@kolab.org', $john); $this->assertSame("The specified email address is invalid.", $result); $result = GroupsController::validateMemberEmail('test123456@localhost', $john); $this->assertSame("The specified email address is invalid.", $result); // Test local non-existing user $result = GroupsController::validateMemberEmail('unknown@kolab.org', $john); $this->assertSame("The specified email address does not exist.", $result); // Test local existing user $result = GroupsController::validateMemberEmail('ned@kolab.org', $john); $this->assertSame(null, $result); // Test existing user, but not in the same account $result = GroupsController::validateMemberEmail('jeroen@jeroen.jeroen', $john); $this->assertSame(null, $result); // Valid address $result = GroupsController::validateMemberEmail('test@google.com', $john); $this->assertSame(null, $result); } } diff --git a/src/tests/Feature/Controller/ResourcesTest.php b/src/tests/Feature/Controller/ResourcesTest.php index 0d89ea57..04268ec8 100644 --- a/src/tests/Feature/Controller/ResourcesTest.php +++ b/src/tests/Feature/Controller/ResourcesTest.php @@ -1,564 +1,563 @@ deleteTestResource('resource-test@kolab.org'); Resource::where('name', 'Test Resource')->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestResource('resource-test@kolab.org'); Resource::where('name', 'Test Resource')->delete(); parent::tearDown(); } /** * Test resource deleting (DELETE /api/v4/resources/) */ public function testDestroy(): void { // First create some groups to delete $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Test unauth access $response = $this->delete("api/v4/resources/{$resource->id}"); $response->assertStatus(401); // Test non-existing resource $response = $this->actingAs($john)->delete("api/v4/resources/abc"); $response->assertStatus(404); // Test access to other user's resource $response = $this->actingAs($jack)->delete("api/v4/resources/{$resource->id}"); $response->assertStatus(403); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test removing a resource $response = $this->actingAs($john)->delete("api/v4/resources/{$resource->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals("Resource deleted successfully.", $json['message']); } /** * Test resources listing (GET /api/v4/resources) */ public function testIndex(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); // Test unauth access $response = $this->get("api/v4/resources"); $response->assertStatus(401); // Test a user with no resources $response = $this->actingAs($jack)->get("/api/v4/resources"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("0 resources have been found.", $json['message']); $this->assertSame([], $json['list']); // Test a user with two resources $response = $this->actingAs($john)->get("/api/v4/resources"); $response->assertStatus(200); $json = $response->json(); $resource = Resource::where('name', 'Conference Room #1')->first(); $this->assertCount(4, $json); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("2 resources have been found.", $json['message']); $this->assertCount(2, $json['list']); $this->assertSame($resource->id, $json['list'][0]['id']); $this->assertSame($resource->email, $json['list'][0]['email']); $this->assertSame($resource->name, $json['list'][0]['name']); $this->assertArrayHasKey('isDeleted', $json['list'][0]); $this->assertArrayHasKey('isActive', $json['list'][0]); - $this->assertArrayHasKey('isLdapReady', $json['list'][0]); - if (\config('app.with_imap')) { - $this->assertArrayHasKey('isImapReady', $json['list'][0]); + $this->assertArrayHasKey('isImapReady', $json['list'][0]); + if (\config('app.with_ldap')) { + $this->assertArrayHasKey('isLdapReady', $json['list'][0]); } // Test that another wallet controller has access to resources $response = $this->actingAs($ned)->get("/api/v4/resources"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("2 resources have been found.", $json['message']); $this->assertCount(2, $json['list']); $this->assertSame($resource->email, $json['list'][0]['email']); } /** * Test resource config update (POST /api/v4/resources//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Test unknown resource id $post = ['invitation_policy' => 'reject']; $response = $this->actingAs($john)->post("/api/v4/resources/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['invitation_policy' => 'reject']; $response = $this->actingAs($jack)->post("/api/v4/resources/{$resource->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['test' => 1]; $response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']); $resource->refresh(); $this->assertNull($resource->getSetting('test')); $this->assertNull($resource->getSetting('invitation_policy')); // Test some valid data $post = ['invitation_policy' => 'reject']; $response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame("Resource settings updated successfully.", $json['message']); $this->assertSame(['invitation_policy' => 'reject'], $resource->fresh()->getConfig()); // Test input validation $post = ['invitation_policy' => 'aaa']; $response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame( "The specified invitation policy is invalid.", $json['errors']['invitation_policy'] ); $this->assertSame(['invitation_policy' => 'reject'], $resource->fresh()->getConfig()); } /** * Test fetching resource data/profile (GET /api/v4/resources/) */ public function testShow(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); $resource->setSetting('invitation_policy', 'reject'); // Test unauthorized access to a profile of other user $response = $this->get("/api/v4/resources/{$resource->id}"); $response->assertStatus(401); // Test unauthorized access to a resource of another user $response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}"); $response->assertStatus(403); // John: Account owner - non-existing resource $response = $this->actingAs($john)->get("/api/v4/resources/abc"); $response->assertStatus(404); // John: Account owner $response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($resource->id, $json['id']); $this->assertSame($resource->email, $json['email']); $this->assertSame($resource->name, $json['name']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isActive', $json); - $this->assertArrayHasKey('isLdapReady', $json); - if (\config('app.with_imap')) { - $this->assertArrayHasKey('isImapReady', $json); + $this->assertArrayHasKey('isImapReady', $json); + if (\config('app.with_ldap')) { + $this->assertArrayHasKey('isLdapReady', $json); } $this->assertSame(['invitation_policy' => 'reject'], $json['config']); $this->assertCount(1, $json['skus']); } /** * Test fetching SKUs list for a resource (GET /resources//skus) */ public function testSkus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Unauth access not allowed $response = $this->get("api/v4/resources/{$resource->id}/skus"); $response->assertStatus(401); // Unauthorized access not allowed $response = $this->actingAs($jack)->get("api/v4/resources/{$resource->id}/skus"); $response->assertStatus(403); $response = $this->actingAs($john)->get("api/v4/resources/{$resource->id}/skus"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSkuElement('resource', $json[0], [ 'prio' => 0, 'type' => 'resource', 'handler' => 'Resource', 'enabled' => true, 'readonly' => true, ]); } /** * Test fetching a resource status (GET /api/v4/resources//status) * and forcing setup process update (?refresh=1) */ public function testStatus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Test unauthorized access $response = $this->get("/api/v4/resources/abc/status"); $response->assertStatus(401); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}/status"); $response->assertStatus(403); $resource->status = Resource::STATUS_NEW | Resource::STATUS_ACTIVE; $resource->save(); // Get resource status $response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}/status"); $response->assertStatus(200); $json = $response->json(); - $this->assertFalse($json['isLdapReady']); $this->assertFalse($json['isReady']); $this->assertFalse($json['isDeleted']); $this->assertTrue($json['isActive']); - if (\config('app.with_imap')) { - $this->assertFalse($json['isImapReady']); - $this->assertCount(7, $json['process']); - } else { - $this->assertCount(6, $json['process']); - } + $this->assertFalse($json['isImapReady']); $this->assertSame('resource-new', $json['process'][0]['label']); $this->assertSame(true, $json['process'][0]['state']); - $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); - $this->assertSame(false, $json['process'][1]['state']); + if (\config('app.with_ldap')) { + $this->assertFalse($json['isLdapReady']); + $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); + $this->assertSame('resource-imap-ready', $json['process'][2]['label']); + $this->assertSame(false, $json['process'][2]['state']); + } else { + $this->assertSame('resource-imap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); + } $this->assertTrue(empty($json['status'])); $this->assertTrue(empty($json['message'])); $this->assertSame('running', $json['processState']); // Make sure the domain is confirmed (other test might unset that status) $domain = $this->getTestDomain('kolab.org'); $domain->status |= \App\Domain::STATUS_CONFIRMED; $domain->save(); $resource->status |= Resource::STATUS_IMAP_READY; $resource->save(); // Now "reboot" the process Queue::fake(); $response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); - $this->assertFalse($json['isLdapReady']); $this->assertFalse($json['isReady']); - if (\config('app.with_imap')) { - $this->assertTrue($json['isImapReady']); - $this->assertCount(7, $json['process']); - } else { - $this->assertCount(6, $json['process']); - } - $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); - $this->assertSame(false, $json['process'][1]['state']); - if (\config('app.with_imap')) { + $this->assertTrue($json['isImapReady']); + if (\config('app.with_ldap')) { + $this->assertFalse($json['isLdapReady']); + $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('resource-imap-ready', $json['process'][2]['label']); + $this->assertSame(true, $json['process'][2]['state']); + } else { + $this->assertSame('resource-imap-ready', $json['process'][1]['label']); + $this->assertSame(true, $json['process'][1]['state']); } - $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); $this->assertSame('waiting', $json['processState']); Queue::assertPushed(\App\Jobs\Resource\CreateJob::class, 1); // Test a case when a domain is not ready Queue::fake(); $domain->status ^= \App\Domain::STATUS_CONFIRMED; $domain->save(); $response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); - $this->assertFalse($json['isLdapReady']); $this->assertFalse($json['isReady']); - if (\config('app.with_imap')) { - $this->assertCount(7, $json['process']); - } else { - $this->assertCount(6, $json['process']); + if (\config('app.with_ldap')) { + $this->assertFalse($json['isLdapReady']); + $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); } - $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); - $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); $this->assertSame('waiting', $json['processState']); Queue::assertPushed(\App\Jobs\Resource\CreateJob::class, 1); } /** * Test ResourcesController::statusInfo() */ public function testStatusInfo(): void { $john = $this->getTestUser('john@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); $resource->status = Resource::STATUS_NEW | Resource::STATUS_ACTIVE; $resource->save(); $domain = $this->getTestDomain('kolab.org'); $domain->status |= \App\Domain::STATUS_CONFIRMED; $domain->save(); $result = ResourcesController::statusInfo($resource); - $this->assertFalse($result['isReady']); - if (\config('app.with_imap')) { - $this->assertCount(7, $result['process']); - } else { - $this->assertCount(6, $result['process']); - } + $this->assertFalse($result['isDone']); $this->assertSame('resource-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('resource-ldap-ready', $result['process'][1]['label']); - $this->assertSame(false, $result['process'][1]['state']); + if (\config('app.with_ldap')) { + $this->assertSame('resource-ldap-ready', $result['process'][1]['label']); + $this->assertSame(false, $result['process'][1]['state']); + $this->assertSame('resource-imap-ready', $result['process'][2]['label']); + $this->assertSame(false, $result['process'][2]['state']); + } else { + $this->assertSame('resource-imap-ready', $result['process'][1]['label']); + $this->assertSame(false, $result['process'][1]['state']); + } $this->assertSame('running', $result['processState']); $resource->created_at = Carbon::now()->subSeconds(181); $resource->save(); $result = ResourcesController::statusInfo($resource); $this->assertSame('failed', $result['processState']); $resource->status |= Resource::STATUS_LDAP_READY | Resource::STATUS_IMAP_READY; $resource->save(); $result = ResourcesController::statusInfo($resource); - $this->assertTrue($result['isReady']); - if (\config('app.with_imap')) { - $this->assertCount(7, $result['process']); - } else { - $this->assertCount(6, $result['process']); - } + $this->assertTrue($result['isDone']); $this->assertSame('resource-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('resource-ldap-ready', $result['process'][1]['label']); - $this->assertSame(true, $result['process'][1]['state']); - $this->assertSame('resource-ldap-ready', $result['process'][1]['label']); - $this->assertSame(true, $result['process'][1]['state']); + if (\config('app.with_ldap')) { + $this->assertSame('resource-ldap-ready', $result['process'][1]['label']); + $this->assertSame(true, $result['process'][1]['state']); + $this->assertSame('resource-imap-ready', $result['process'][2]['label']); + $this->assertSame(true, $result['process'][2]['state']); + } else { + $this->assertSame('resource-imap-ready', $result['process'][1]['label']); + $this->assertSame(true, $result['process'][1]['state']); + } $this->assertSame('done', $result['processState']); } /** * Test resource creation (POST /api/v4/resources) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test unauth request $response = $this->post("/api/v4/resources", []); $response->assertStatus(401); // Test non-controller user $response = $this->actingAs($jack)->post("/api/v4/resources", []); $response->assertStatus(403); // Test empty request $response = $this->actingAs($john)->post("/api/v4/resources", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The name field is required.", $json['errors']['name'][0]); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); // Test too long name $post = ['domain' => 'kolab.org', 'name' => str_repeat('A', 192)]; $response = $this->actingAs($john)->post("/api/v4/resources", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame("The name may not be greater than 191 characters.", $json['errors']['name'][0]); $this->assertCount(1, $json['errors']); // Test successful resource creation $post['name'] = 'Test Resource'; $response = $this->actingAs($john)->post("/api/v4/resources", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Resource created successfully.", $json['message']); $this->assertCount(2, $json); $resource = Resource::where('name', $post['name'])->first(); $this->assertInstanceOf(Resource::class, $resource); $this->assertTrue($john->resources()->get()->contains($resource)); // Resource name must be unique within a domain $response = $this->actingAs($john)->post("/api/v4/resources", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame("The specified name is not available.", $json['errors']['name'][0]); } /** * Test resource update (PUT /api/v4/resources/) */ public function testUpdate(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Test unauthorized update $response = $this->get("/api/v4/resources/{$resource->id}", []); $response->assertStatus(401); // Test unauthorized update $response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}", []); $response->assertStatus(403); // Name change $post = [ 'name' => 'Test Res', ]; $response = $this->actingAs($john)->put("/api/v4/resources/{$resource->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Resource updated successfully.", $json['message']); $this->assertCount(2, $json); $resource->refresh(); $this->assertSame($post['name'], $resource->name); } } diff --git a/src/tests/Feature/Controller/SharedFoldersTest.php b/src/tests/Feature/Controller/SharedFoldersTest.php index 70388614..75ebdb60 100644 --- a/src/tests/Feature/Controller/SharedFoldersTest.php +++ b/src/tests/Feature/Controller/SharedFoldersTest.php @@ -1,650 +1,643 @@ deleteTestSharedFolder('folder-test@kolab.org'); SharedFolder::where('name', 'like', 'Test_Folder')->forceDelete(); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestSharedFolder('folder-test@kolab.org'); SharedFolder::where('name', 'like', 'Test_Folder')->forceDelete(); parent::tearDown(); } /** * Test resource deleting (DELETE /api/v4/resources/) */ public function testDestroy(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Test unauth access $response = $this->delete("api/v4/shared-folders/{$folder->id}"); $response->assertStatus(401); // Test non-existing folder $response = $this->actingAs($john)->delete("api/v4/shared-folders/abc"); $response->assertStatus(404); // Test access to other user's folder $response = $this->actingAs($jack)->delete("api/v4/shared-folders/{$folder->id}"); $response->assertStatus(403); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test removing a folder $response = $this->actingAs($john)->delete("api/v4/shared-folders/{$folder->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals("Shared folder deleted successfully.", $json['message']); } /** * Test shared folders listing (GET /api/v4/shared-folders) */ public function testIndex(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); // Test unauth access $response = $this->get("api/v4/shared-folders"); $response->assertStatus(401); // Test a user with no shared folders $response = $this->actingAs($jack)->get("/api/v4/shared-folders"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(0, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("0 shared folders have been found.", $json['message']); $this->assertSame([], $json['list']); // Test a user with two shared folders $response = $this->actingAs($john)->get("/api/v4/shared-folders"); $response->assertStatus(200); $json = $response->json(); $folder = SharedFolder::where('name', 'Calendar')->first(); $this->assertCount(4, $json); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("2 shared folders have been found.", $json['message']); $this->assertCount(2, $json['list']); $this->assertSame($folder->id, $json['list'][0]['id']); $this->assertSame($folder->email, $json['list'][0]['email']); $this->assertSame($folder->name, $json['list'][0]['name']); $this->assertSame($folder->type, $json['list'][0]['type']); $this->assertArrayHasKey('isDeleted', $json['list'][0]); $this->assertArrayHasKey('isActive', $json['list'][0]); - $this->assertArrayHasKey('isLdapReady', $json['list'][0]); - if (\config('app.with_imap')) { - $this->assertArrayHasKey('isImapReady', $json['list'][0]); + $this->assertArrayHasKey('isImapReady', $json['list'][0]); + if (\config('app.with_ldap')) { + $this->assertArrayHasKey('isLdapReady', $json['list'][0]); } // Test that another wallet controller has access to shared folders $response = $this->actingAs($ned)->get("/api/v4/shared-folders"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(4, $json); $this->assertSame(2, $json['count']); $this->assertSame(false, $json['hasMore']); $this->assertSame("2 shared folders have been found.", $json['message']); $this->assertCount(2, $json['list']); $this->assertSame($folder->email, $json['list'][0]['email']); } /** * Test shared folder config update (POST /api/v4/shared-folders//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Test unknown resource id $post = ['acl' => ['john@kolab.org, full']]; $response = $this->actingAs($john)->post("/api/v4/shared-folders/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $response = $this->actingAs($jack)->post("/api/v4/shared-folders/{$folder->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['test' => 1]; $response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']); $folder->refresh(); $this->assertNull($folder->getSetting('test')); $this->assertNull($folder->getSetting('acl')); // Test some valid data $post = ['acl' => ['john@kolab.org, full']]; $response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame("Shared folder settings updated successfully.", $json['message']); $this->assertSame(['acl' => $post['acl']], $folder->fresh()->getConfig()); // Test input validation $post = ['acl' => ['john@kolab.org, full', 'test, full']]; $response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['acl']); $this->assertSame( "The specified email address is invalid.", $json['errors']['acl'][1] ); $this->assertSame(['acl' => ['john@kolab.org, full']], $folder->fresh()->getConfig()); } /** * Test fetching shared folder data/profile (GET /api/v4/shared-folders/) */ public function testShow(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); $folder->setSetting('acl', '["anyone, full"]'); $folder->setAliases(['folder-alias@kolab.org']); // Test unauthenticated access $response = $this->get("/api/v4/shared-folders/{$folder->id}"); $response->assertStatus(401); // Test unauthorized access to a shared folder of another user $response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}"); $response->assertStatus(403); // John: Account owner - non-existing folder $response = $this->actingAs($john)->get("/api/v4/shared-folders/abc"); $response->assertStatus(404); // John: Account owner $response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($folder->id, $json['id']); $this->assertSame($folder->email, $json['email']); $this->assertSame($folder->name, $json['name']); $this->assertSame($folder->type, $json['type']); $this->assertSame(['folder-alias@kolab.org'], $json['aliases']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isActive', $json); - $this->assertArrayHasKey('isLdapReady', $json); - if (\config('app.with_imap')) { - $this->assertArrayHasKey('isImapReady', $json); + $this->assertArrayHasKey('isImapReady', $json); + if (\config('app.with_ldap')) { + $this->assertArrayHasKey('isLdapReady', $json); } $this->assertSame(['acl' => ['anyone, full']], $json['config']); $this->assertCount(1, $json['skus']); } /** * Test fetching SKUs list for a shared folder (GET /shared-folders//skus) */ public function testSkus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Unauth access not allowed $response = $this->get("api/v4/shared-folders/{$folder->id}/skus"); $response->assertStatus(401); // Unauthorized access not allowed $response = $this->actingAs($jack)->get("api/v4/shared-folders/{$folder->id}/skus"); $response->assertStatus(403); // Non-existing folder $response = $this->actingAs($john)->get("api/v4/shared-folders/non-existing/skus"); $response->assertStatus(404); $response = $this->actingAs($john)->get("api/v4/shared-folders/{$folder->id}/skus"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(1, $json); $this->assertSkuElement('shared-folder', $json[0], [ 'prio' => 0, 'type' => 'sharedFolder', 'handler' => 'SharedFolder', 'enabled' => true, 'readonly' => true, ]); } /** * Test fetching a shared folder status (GET /api/v4/shared-folders//status) * and forcing setup process update (?refresh=1) */ public function testStatus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Test unauthorized access $response = $this->get("/api/v4/shared-folders/abc/status"); $response->assertStatus(401); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}/status"); $response->assertStatus(403); $folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE; $folder->save(); // Get resource status $response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}/status"); $response->assertStatus(200); $json = $response->json(); - $this->assertFalse($json['isLdapReady']); + $this->assertFalse($json['isImapReady']); $this->assertFalse($json['isReady']); $this->assertFalse($json['isDeleted']); $this->assertTrue($json['isActive']); - if (\config('app.with_imap')) { - $this->assertFalse($json['isImapReady']); - $this->assertCount(7, $json['process']); - } else { - $this->assertCount(6, $json['process']); - } - $this->assertSame('shared-folder-new', $json['process'][0]['label']); - $this->assertSame(true, $json['process'][0]['state']); - $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); - $this->assertSame(false, $json['process'][1]['state']); + $this->assertSame('running', $json['processState']); $this->assertTrue(empty($json['status'])); $this->assertTrue(empty($json['message'])); - $this->assertSame('running', $json['processState']); + $this->assertSame('shared-folder-new', $json['process'][0]['label']); + $this->assertSame(true, $json['process'][0]['state']); + if (\config('app.with_ldap')) { + $this->assertFalse($json['isLdapReady']); + $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); + } // Make sure the domain is confirmed (other test might unset that status) $domain = $this->getTestDomain('kolab.org'); $domain->status |= \App\Domain::STATUS_CONFIRMED; $domain->save(); $folder->status |= SharedFolder::STATUS_IMAP_READY; $folder->save(); // Now "reboot" the process Queue::fake(); $response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); - $this->assertFalse($json['isLdapReady']); + $this->assertTrue($json['isImapReady']); $this->assertFalse($json['isReady']); - if (\config('app.with_imap')) { - $this->assertTrue($json['isImapReady']); - $this->assertCount(7, $json['process']); - } else { - $this->assertCount(6, $json['process']); - } - $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); - $this->assertSame(false, $json['process'][1]['state']); - if (\config('app.with_imap')) { + if (\config('app.with_ldap')) { + $this->assertFalse($json['isLdapReady']); + $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('shared-folder-imap-ready', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); + } else { + $this->assertSame('shared-folder-imap-ready', $json['process'][1]['label']); + $this->assertSame(true, $json['process'][1]['state']); } $this->assertSame('success', $json['status']); $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); $this->assertSame('waiting', $json['processState']); Queue::assertPushed(\App\Jobs\SharedFolder\CreateJob::class, 1); // Test a case when a domain is not ready Queue::fake(); $domain->status ^= \App\Domain::STATUS_CONFIRMED; $domain->save(); $response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); - $this->assertFalse($json['isLdapReady']); + $this->assertTrue($json['isImapReady']); $this->assertFalse($json['isReady']); - if (\config('app.with_imap')) { - $this->assertCount(7, $json['process']); + if (\config('app.with_ldap')) { + $this->assertFalse($json['isLdapReady']); + $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); } else { - $this->assertCount(6, $json['process']); + $this->assertSame('shared-folder-imap-ready', $json['process'][1]['label']); + $this->assertSame(true, $json['process'][1]['state']); } - $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); - $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); $this->assertSame('waiting', $json['processState']); Queue::assertPushed(\App\Jobs\SharedFolder\CreateJob::class, 1); } /** * Test SharedFoldersController::statusInfo() */ public function testStatusInfo(): void { $john = $this->getTestUser('john@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); $folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE; $folder->save(); $domain = $this->getTestDomain('kolab.org'); $domain->status |= \App\Domain::STATUS_CONFIRMED; $domain->save(); $result = SharedFoldersController::statusInfo($folder); - $this->assertFalse($result['isReady']); - if (\config('app.with_imap')) { - $this->assertCount(7, $result['process']); - } else { - $this->assertCount(6, $result['process']); - } + $this->assertFalse($result['isDone']); + $this->assertSame('running', $result['processState']); $this->assertSame('shared-folder-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('shared-folder-ldap-ready', $result['process'][1]['label']); - $this->assertSame(false, $result['process'][1]['state']); - $this->assertSame('running', $result['processState']); + if (\config('app.with_ldap')) { + $this->assertSame('shared-folder-ldap-ready', $result['process'][1]['label']); + $this->assertSame(false, $result['process'][1]['state']); + } $folder->created_at = Carbon::now()->subSeconds(181); $folder->save(); $result = SharedFoldersController::statusInfo($folder); $this->assertSame('failed', $result['processState']); $folder->status |= SharedFolder::STATUS_LDAP_READY | SharedFolder::STATUS_IMAP_READY; $folder->save(); $result = SharedFoldersController::statusInfo($folder); - $this->assertTrue($result['isReady']); - if (\config('app.with_imap')) { - $this->assertCount(7, $result['process']); - } else { - $this->assertCount(6, $result['process']); - } + $this->assertTrue($result['isDone']); + $this->assertSame('done', $result['processState']); $this->assertSame('shared-folder-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('shared-folder-ldap-ready', $result['process'][1]['label']); - $this->assertSame(true, $result['process'][1]['state']); - $this->assertSame('shared-folder-ldap-ready', $result['process'][1]['label']); - $this->assertSame(true, $result['process'][1]['state']); - $this->assertSame('done', $result['processState']); + if (\config('app.with_ldap')) { + $this->assertSame('shared-folder-ldap-ready', $result['process'][1]['label']); + $this->assertSame(true, $result['process'][1]['state']); + $this->assertSame('shared-folder-imap-ready', $result['process'][2]['label']); + $this->assertSame(true, $result['process'][2]['state']); + } else { + $this->assertSame('shared-folder-imap-ready', $result['process'][1]['label']); + $this->assertSame(true, $result['process'][1]['state']); + } } /** * Test shared folder creation (POST /api/v4/shared-folders) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test unauth request $response = $this->post("/api/v4/shared-folders", []); $response->assertStatus(401); // Test non-controller user $response = $this->actingAs($jack)->post("/api/v4/shared-folders", []); $response->assertStatus(403); // Test empty request $response = $this->actingAs($john)->post("/api/v4/shared-folders", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The name field is required.", $json['errors']['name'][0]); $this->assertSame("The type field is required.", $json['errors']['type'][0]); $this->assertCount(2, $json); $this->assertCount(2, $json['errors']); // Test too long name, invalid alias domain $post = [ 'domain' => 'kolab.org', 'name' => str_repeat('A', 192), 'type' => 'unknown', 'aliases' => ['folder-alias@unknown.org'], ]; $response = $this->actingAs($john)->post("/api/v4/shared-folders", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame(["The name may not be greater than 191 characters."], $json['errors']['name']); $this->assertSame(["The specified type is invalid."], $json['errors']['type']); $this->assertSame(["The specified domain is invalid."], $json['errors']['aliases']); $this->assertCount(3, $json['errors']); // Test successful folder creation $post['name'] = 'Test Folder'; $post['type'] = 'event'; $post['aliases'] = []; $response = $this->actingAs($john)->post("/api/v4/shared-folders", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Shared folder created successfully.", $json['message']); $this->assertCount(2, $json); $folder = SharedFolder::where('name', $post['name'])->first(); $this->assertInstanceOf(SharedFolder::class, $folder); $this->assertSame($post['type'], $folder->type); $this->assertTrue($john->sharedFolders()->get()->contains($folder)); $this->assertSame([], $folder->aliases()->pluck('alias')->all()); // Shared folder name must be unique within a domain $post['type'] = 'mail'; $response = $this->actingAs($john)->post("/api/v4/shared-folders", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame("The specified name is not available.", $json['errors']['name'][0]); $folder->forceDelete(); // Test successful folder creation with aliases $post['name'] = 'Test Folder'; $post['type'] = 'mail'; $post['aliases'] = ['folder-alias@kolab.org']; $response = $this->actingAs($john)->post("/api/v4/shared-folders", $post); $json = $response->json(); $response->assertStatus(200); $folder = SharedFolder::where('name', $post['name'])->first(); $this->assertSame(['folder-alias@kolab.org'], $folder->aliases()->pluck('alias')->all()); $folder->forceDelete(); // Test handling subfolders and lmtp alias email $post['name'] = 'Test/Folder'; $post['type'] = 'mail'; $post['aliases'] = ['shared+shared/Test/Folder@kolab.org']; $response = $this->actingAs($john)->post("/api/v4/shared-folders", $post); $json = $response->json(); $response->assertStatus(200); $folder = SharedFolder::where('name', $post['name'])->first(); $this->assertSame(['shared+shared/Test/Folder@kolab.org'], $folder->aliases()->pluck('alias')->all()); } /** * Test shared folder update (PUT /api/v4/shared-folders/getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Test unauthorized update $response = $this->get("/api/v4/shared-folders/{$folder->id}", []); $response->assertStatus(401); // Test unauthorized update $response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}", []); $response->assertStatus(403); // Name change $post = [ 'name' => 'Test Res', ]; $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Shared folder updated successfully.", $json['message']); $this->assertCount(2, $json); $folder->refresh(); $this->assertSame($post['name'], $folder->name); // Aliases with error $post['aliases'] = ['folder-alias1@kolab.org', 'folder-alias2@unknown.com']; $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['aliases']); $this->assertSame("The specified domain is invalid.", $json['errors']['aliases'][1]); $this->assertSame([], $folder->aliases()->pluck('alias')->all()); // Aliases with success expected $post['aliases'] = ['folder-alias1@kolab.org', 'folder-alias2@kolab.org']; $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Shared folder updated successfully.", $json['message']); $this->assertCount(2, $json); $this->assertSame($post['aliases'], $folder->aliases()->pluck('alias')->all()); // All aliases removal $post['aliases'] = []; $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post); $response->assertStatus(200); $this->assertSame($post['aliases'], $folder->aliases()->pluck('alias')->all()); } } diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php index b5c85e45..ff719999 100644 --- a/src/tests/Feature/Controller/UsersTest.php +++ b/src/tests/Feature/Controller/UsersTest.php @@ -1,1669 +1,1666 @@ clearBetaEntitlements(); $this->deleteTestUser('jane@kolabnow.com'); $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); $this->deleteTestUser('UsersControllerTest2@userscontroller.com'); $this->deleteTestUser('UsersControllerTest3@userscontroller.com'); $this->deleteTestUser('UserEntitlement2A@UserEntitlement.com'); $this->deleteTestUser('john2.doe2@kolab.org'); $this->deleteTestUser('deleted@kolab.org'); $this->deleteTestUser('deleted@kolabnow.com'); $this->deleteTestDomain('userscontroller.com'); $this->deleteTestGroup('group-test@kolabnow.com'); $this->deleteTestGroup('group-test@kolab.org'); $this->deleteTestSharedFolder('folder-test@kolabnow.com'); $this->deleteTestResource('resource-test@kolabnow.com'); Sku::where('title', 'test')->delete(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->discount()->dissociate(); $wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete(); $wallet->save(); $user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete(); $user->status |= User::STATUS_IMAP_READY; $user->save(); } /** * {@inheritDoc} */ public function tearDown(): void { $this->clearBetaEntitlements(); $this->deleteTestUser('jane@kolabnow.com'); $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); $this->deleteTestUser('UsersControllerTest2@userscontroller.com'); $this->deleteTestUser('UsersControllerTest3@userscontroller.com'); $this->deleteTestUser('UserEntitlement2A@UserEntitlement.com'); $this->deleteTestUser('john2.doe2@kolab.org'); $this->deleteTestUser('deleted@kolab.org'); $this->deleteTestUser('deleted@kolabnow.com'); $this->deleteTestDomain('userscontroller.com'); $this->deleteTestGroup('group-test@kolabnow.com'); $this->deleteTestGroup('group-test@kolab.org'); $this->deleteTestSharedFolder('folder-test@kolabnow.com'); $this->deleteTestResource('resource-test@kolabnow.com'); Sku::where('title', 'test')->delete(); $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->discount()->dissociate(); $wallet->settings()->whereIn('key', ['mollie_id', 'stripe_id'])->delete(); $wallet->save(); $user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete(); $user->status |= User::STATUS_IMAP_READY; $user->save(); parent::tearDown(); } /** * Test user deleting (DELETE /api/v4/users/) */ public function testDestroy(): void { // First create some users/accounts to delete $package_kolab = \App\Package::where('title', 'kolab')->first(); $package_domain = \App\Package::where('title', 'domain-hosting')->first(); $john = $this->getTestUser('john@kolab.org'); $user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com'); $user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $user1->assignPackage($package_kolab); $domain->assignPackage($package_domain, $user1); $user1->assignPackage($package_kolab, $user2); $user1->assignPackage($package_kolab, $user3); // Test unauth access $response = $this->delete("api/v4/users/{$user2->id}"); $response->assertStatus(401); // Test access to other user/account $response = $this->actingAs($john)->delete("api/v4/users/{$user2->id}"); $response->assertStatus(403); $response = $this->actingAs($john)->delete("api/v4/users/{$user1->id}"); $response->assertStatus(403); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test that non-controller cannot remove himself $response = $this->actingAs($user3)->delete("api/v4/users/{$user3->id}"); $response->assertStatus(403); // Test removing a non-controller user $response = $this->actingAs($user1)->delete("api/v4/users/{$user3->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('User deleted successfully.', $json['message']); // Test removing self (an account with users) $response = $this->actingAs($user1)->delete("api/v4/users/{$user1->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('User deleted successfully.', $json['message']); } /** * Test user deleting (DELETE /api/v4/users/) */ public function testDestroyByController(): void { // Create an account with additional controller - $user2 $package_kolab = \App\Package::where('title', 'kolab')->first(); $package_domain = \App\Package::where('title', 'domain-hosting')->first(); $user1 = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $user2 = $this->getTestUser('UsersControllerTest2@userscontroller.com'); $user3 = $this->getTestUser('UsersControllerTest3@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $user1->assignPackage($package_kolab); $domain->assignPackage($package_domain, $user1); $user1->assignPackage($package_kolab, $user2); $user1->assignPackage($package_kolab, $user3); $user1->wallets()->first()->addController($user2); // TODO/FIXME: // For now controller can delete himself, as well as // the whole account he has control to, including the owner // Probably he should not be able to do none of those // However, this is not 0-regression scenario as we // do not fully support additional controllers. //$response = $this->actingAs($user2)->delete("api/v4/users/{$user2->id}"); //$response->assertStatus(403); $response = $this->actingAs($user2)->delete("api/v4/users/{$user3->id}"); $response->assertStatus(200); $response = $this->actingAs($user2)->delete("api/v4/users/{$user1->id}"); $response->assertStatus(200); // Note: More detailed assertions in testDestroy() above $this->assertTrue($user1->fresh()->trashed()); $this->assertTrue($user2->fresh()->trashed()); $this->assertTrue($user3->fresh()->trashed()); } /** * Test user listing (GET /api/v4/users) */ public function testIndex(): void { // Test unauth access $response = $this->get("api/v4/users"); $response->assertStatus(401); $jack = $this->getTestUser('jack@kolab.org'); $joe = $this->getTestUser('joe@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $response = $this->actingAs($jack)->get("/api/v4/users"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(false, $json['hasMore']); $this->assertSame(0, $json['count']); $this->assertCount(0, $json['list']); $response = $this->actingAs($john)->get("/api/v4/users"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(false, $json['hasMore']); $this->assertSame(4, $json['count']); $this->assertCount(4, $json['list']); $this->assertSame($jack->email, $json['list'][0]['email']); $this->assertSame($joe->email, $json['list'][1]['email']); $this->assertSame($john->email, $json['list'][2]['email']); $this->assertSame($ned->email, $json['list'][3]['email']); // Values below are tested by Unit tests $this->assertArrayHasKey('isDeleted', $json['list'][0]); $this->assertArrayHasKey('isDegraded', $json['list'][0]); $this->assertArrayHasKey('isAccountDegraded', $json['list'][0]); $this->assertArrayHasKey('isSuspended', $json['list'][0]); $this->assertArrayHasKey('isActive', $json['list'][0]); $this->assertArrayHasKey('isReady', $json['list'][0]); + $this->assertArrayHasKey('isImapReady', $json['list'][0]); if (\config('app.with_ldap')) { $this->assertArrayHasKey('isLdapReady', $json['list'][0]); - } - if (\config('app.with_imap')) { - $this->assertArrayHasKey('isImapReady', $json['list'][0]); + } else { + $this->assertArrayNotHasKey('isLdapReady', $json['list'][0]); } $response = $this->actingAs($ned)->get("/api/v4/users"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(false, $json['hasMore']); $this->assertSame(4, $json['count']); $this->assertCount(4, $json['list']); $this->assertSame($jack->email, $json['list'][0]['email']); $this->assertSame($joe->email, $json['list'][1]['email']); $this->assertSame($john->email, $json['list'][2]['email']); $this->assertSame($ned->email, $json['list'][3]['email']); // Search by user email $response = $this->actingAs($john)->get("/api/v4/users?search=jack@k"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(false, $json['hasMore']); $this->assertSame(1, $json['count']); $this->assertCount(1, $json['list']); $this->assertSame($jack->email, $json['list'][0]['email']); // Search by alias $response = $this->actingAs($john)->get("/api/v4/users?search=monster"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(false, $json['hasMore']); $this->assertSame(1, $json['count']); $this->assertCount(1, $json['list']); $this->assertSame($joe->email, $json['list'][0]['email']); // Search by name $response = $this->actingAs($john)->get("/api/v4/users?search=land"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(false, $json['hasMore']); $this->assertSame(1, $json['count']); $this->assertCount(1, $json['list']); $this->assertSame($ned->email, $json['list'][0]['email']); // TODO: Test paging } /** * Test fetching user data/profile (GET /api/v4/users/) */ public function testShow(): void { $userA = $this->getTestUser('UserEntitlement2A@UserEntitlement.com'); // Test getting profile of self $response = $this->actingAs($userA)->get("/api/v4/users/{$userA->id}"); $json = $response->json(); $response->assertStatus(200); $this->assertEquals($userA->id, $json['id']); $this->assertEquals($userA->email, $json['email']); $this->assertTrue(is_array($json['statusInfo'])); $this->assertTrue(is_array($json['settings'])); $this->assertTrue($json['config']['greylist_enabled']); $this->assertFalse($json['config']['guam_enabled']); $this->assertSame([], $json['skus']); $this->assertSame([], $json['aliases']); // Values below are tested by Unit tests $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isDegraded', $json); $this->assertArrayHasKey('isAccountDegraded', $json); $this->assertArrayHasKey('isSuspended', $json); $this->assertArrayHasKey('isActive', $json); $this->assertArrayHasKey('isReady', $json); + $this->assertArrayHasKey('isImapReady', $json); if (\config('app.with_ldap')) { $this->assertArrayHasKey('isLdapReady', $json); - } - if (\config('app.with_imap')) { - $this->assertArrayHasKey('isImapReady', $json); + } else { + $this->assertArrayNotHasKey('isLdapReady', $json); } $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); // Test unauthorized access to a profile of other user $response = $this->actingAs($jack)->get("/api/v4/users/{$userA->id}"); $response->assertStatus(403); // Test authorized access to a profile of other user // Ned: Additional account controller $response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame(['john.doe@kolab.org'], $json['aliases']); $response = $this->actingAs($ned)->get("/api/v4/users/{$jack->id}"); $response->assertStatus(200); // John: Account owner $response = $this->actingAs($john)->get("/api/v4/users/{$jack->id}"); $response->assertStatus(200); $response = $this->actingAs($john)->get("/api/v4/users/{$ned->id}"); $response->assertStatus(200); $json = $response->json(); $storage_sku = Sku::withEnvTenantContext()->where('title', 'storage')->first(); $groupware_sku = Sku::withEnvTenantContext()->where('title', 'groupware')->first(); $mailbox_sku = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); $secondfactor_sku = Sku::withEnvTenantContext()->where('title', '2fa')->first(); $this->assertCount(5, $json['skus']); $this->assertSame(5, $json['skus'][$storage_sku->id]['count']); $this->assertSame([0,0,0,0,0], $json['skus'][$storage_sku->id]['costs']); $this->assertSame(1, $json['skus'][$groupware_sku->id]['count']); $this->assertSame([490], $json['skus'][$groupware_sku->id]['costs']); $this->assertSame(1, $json['skus'][$mailbox_sku->id]['count']); $this->assertSame([500], $json['skus'][$mailbox_sku->id]['costs']); $this->assertSame(1, $json['skus'][$secondfactor_sku->id]['count']); $this->assertSame([0], $json['skus'][$secondfactor_sku->id]['costs']); $this->assertSame([], $json['aliases']); } /** * Test fetching SKUs list for a user (GET /users//skus) */ public function testSkus(): void { $user = $this->getTestUser('john@kolab.org'); // Unauth access not allowed $response = $this->get("api/v4/users/{$user->id}/skus"); $response->assertStatus(401); // Create an sku for another tenant, to make sure it is not included in the result $nsku = Sku::create([ 'title' => 'test', 'name' => 'Test', 'description' => '', 'active' => true, 'cost' => 100, 'handler_class' => 'Mailbox', ]); $tenant = Tenant::whereNotIn('id', [\config('app.tenant_id')])->first(); $nsku->tenant_id = $tenant->id; $nsku->save(); $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(5, $json); $this->assertSkuElement('mailbox', $json[0], [ 'prio' => 100, 'type' => 'user', 'handler' => 'Mailbox', 'enabled' => true, 'readonly' => true, ]); $this->assertSkuElement('storage', $json[1], [ 'prio' => 90, 'type' => 'user', 'handler' => 'Storage', 'enabled' => true, 'readonly' => true, 'range' => [ 'min' => 5, 'max' => 100, 'unit' => 'GB', ] ]); $this->assertSkuElement('groupware', $json[2], [ 'prio' => 80, 'type' => 'user', 'handler' => 'Groupware', 'enabled' => false, 'readonly' => false, ]); $this->assertSkuElement('activesync', $json[3], [ 'prio' => 70, 'type' => 'user', 'handler' => 'Activesync', 'enabled' => false, 'readonly' => false, 'required' => ['Groupware'], ]); $this->assertSkuElement('2fa', $json[4], [ 'prio' => 60, 'type' => 'user', 'handler' => 'Auth2F', 'enabled' => false, 'readonly' => false, 'forbidden' => ['Activesync'], ]); // Test inclusion of beta SKUs $sku = Sku::withEnvTenantContext()->where('title', 'beta')->first(); $user->assignSku($sku); $response = $this->actingAs($user)->get("api/v4/users/{$user->id}/skus"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(6, $json); $this->assertSkuElement('beta', $json[5], [ 'prio' => 10, 'type' => 'user', 'handler' => 'Beta', 'enabled' => false, 'readonly' => false, ]); } /** * Test fetching user status (GET /api/v4/users//status) * and forcing setup process update (?refresh=1) * * @group imap * @group dns */ public function testStatus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/users/{$john->id}/status"); $response->assertStatus(403); $john->status &= ~User::STATUS_IMAP_READY; $john->status &= ~User::STATUS_LDAP_READY; $john->save(); // Get user status $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse($json['isReady']); + $this->assertFalse($json['isImapReady']); + $this->assertTrue(empty($json['status'])); + $this->assertTrue(empty($json['message'])); if (\config('app.with_ldap')) { $this->assertFalse($json['isLdapReady']); - } else { - $this->assertArrayNotHasKey('isLdapReady', $json); - } - - if (\config('app.with_imap')) { - $this->assertFalse($json['isImapReady']); + $this->assertSame('user-ldap-ready', $json['process'][1]['label']); + $this->assertFalse($json['process'][1]['state']); $this->assertSame('user-imap-ready', $json['process'][2]['label']); $this->assertFalse($json['process'][2]['state']); } else { - $this->assertArrayNotHasKey('isImapReady', $json); + $this->assertArrayNotHasKey('isLdapReady', $json); + $this->assertSame('user-imap-ready', $json['process'][1]['label']); + $this->assertFalse($json['process'][1]['state']); } - $this->assertTrue(empty($json['status'])); - $this->assertTrue(empty($json['message'])); // Make sure the domain is confirmed (other test might unset that status) $domain = $this->getTestDomain('kolab.org'); $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); // Now "reboot" the process Queue::fake(); $response = $this->actingAs($john)->get("/api/v4/users/{$john->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); - $this->assertFalse($json['isLdapReady']); + $this->assertFalse($json['isImapReady']); $this->assertFalse($json['isReady']); - if (\config('app.with_imap')) { - $this->assertFalse($json['isImapReady']); + $this->assertSame('success', $json['status']); + $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); + + if (\config('app.with_ldap')) { + $this->assertFalse($json['isLdapReady']); + $this->assertSame('user-ldap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); $this->assertSame('user-imap-ready', $json['process'][2]['label']); $this->assertSame(false, $json['process'][2]['state']); + } else { + $this->assertSame('user-imap-ready', $json['process'][1]['label']); + $this->assertSame(false, $json['process'][1]['state']); } - $this->assertSame('success', $json['status']); - $this->assertSame('Setup process has been pushed. Please wait.', $json['message']); Queue::assertPushed(\App\Jobs\User\CreateJob::class, 1); } /** * Test UsersController::statusInfo() */ public function testStatusInfo(): void { $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $user->created_at = Carbon::now(); $user->status = User::STATUS_NEW; $user->save(); $result = UsersController::statusInfo($user); - $this->assertFalse($result['isReady']); + $this->assertFalse($result['isDone']); $this->assertSame([], $result['skus']); - if (\config('app.with_imap')) { - $this->assertCount(3, $result['process']); - } else { - $this->assertCount(2, $result['process']); - } + $this->assertCount(3, $result['process']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('user-ldap-ready', $result['process'][1]['label']); - $this->assertSame(false, $result['process'][1]['state']); - if (\config('app.with_imap')) { + if (\config('app.with_ldap')) { + $this->assertSame('user-ldap-ready', $result['process'][1]['label']); + $this->assertSame(false, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(false, $result['process'][2]['state']); + } else { + $this->assertSame('user-imap-ready', $result['process'][1]['label']); + $this->assertSame(false, $result['process'][1]['state']); } $this->assertSame('running', $result['processState']); $this->assertTrue($result['enableRooms']); $this->assertFalse($result['enableBeta']); $user->created_at = Carbon::now()->subSeconds(181); $user->save(); $result = UsersController::statusInfo($user); $this->assertSame('failed', $result['processState']); $user->status |= User::STATUS_LDAP_READY | User::STATUS_IMAP_READY; $user->save(); $result = UsersController::statusInfo($user); - $this->assertTrue($result['isReady']); - if (\config('app.with_imap')) { - $this->assertCount(3, $result['process']); - } else { - $this->assertCount(2, $result['process']); - } + $this->assertTrue($result['isDone']); + $this->assertCount(3, $result['process']); + $this->assertSame('done', $result['processState']); $this->assertSame('user-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('user-ldap-ready', $result['process'][1]['label']); - $this->assertSame(true, $result['process'][1]['state']); - if (\config('app.with_imap')) { + if (\config('app.with_ldap')) { + $this->assertSame('user-ldap-ready', $result['process'][1]['label']); + $this->assertSame(true, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(true, $result['process'][2]['state']); + } else { + $this->assertSame('user-imap-ready', $result['process'][1]['label']); + $this->assertSame(true, $result['process'][1]['state']); } - $this->assertSame('done', $result['processState']); $domain->status |= Domain::STATUS_VERIFIED; $domain->type = Domain::TYPE_EXTERNAL; $domain->save(); $result = UsersController::statusInfo($user); - $this->assertFalse($result['isReady']); + $this->assertFalse($result['isDone']); $this->assertSame([], $result['skus']); - - if (\config('app.with_imap')) { - $this->assertCount(7, $result['process']); - $this->assertSame('user-new', $result['process'][0]['label']); - $this->assertSame(true, $result['process'][0]['state']); + $this->assertCount(7, $result['process']); + $this->assertSame('user-new', $result['process'][0]['label']); + $this->assertSame(true, $result['process'][0]['state']); + if (\config('app.with_ldap')) { $this->assertSame('user-ldap-ready', $result['process'][1]['label']); $this->assertSame(true, $result['process'][1]['state']); $this->assertSame('user-imap-ready', $result['process'][2]['label']); $this->assertSame(true, $result['process'][2]['state']); $this->assertSame('domain-new', $result['process'][3]['label']); $this->assertSame(true, $result['process'][3]['state']); $this->assertSame('domain-ldap-ready', $result['process'][4]['label']); $this->assertSame(false, $result['process'][4]['state']); $this->assertSame('domain-verified', $result['process'][5]['label']); $this->assertSame(true, $result['process'][5]['state']); $this->assertSame('domain-confirmed', $result['process'][6]['label']); $this->assertSame(false, $result['process'][6]['state']); } else { - $this->assertCount(6, $result['process']); - $this->assertSame('user-new', $result['process'][0]['label']); - $this->assertSame(true, $result['process'][0]['state']); - $this->assertSame('user-ldap-ready', $result['process'][1]['label']); + $this->assertSame('user-imap-ready', $result['process'][1]['label']); $this->assertSame(true, $result['process'][1]['state']); $this->assertSame('domain-new', $result['process'][2]['label']); $this->assertSame(true, $result['process'][2]['state']); - $this->assertSame('domain-ldap-ready', $result['process'][3]['label']); - $this->assertSame(false, $result['process'][3]['state']); - $this->assertSame('domain-verified', $result['process'][4]['label']); - $this->assertSame(true, $result['process'][4]['state']); - $this->assertSame('domain-confirmed', $result['process'][5]['label']); - $this->assertSame(false, $result['process'][5]['state']); + $this->assertSame('domain-verified', $result['process'][3]['label']); + $this->assertSame(true, $result['process'][3]['state']); + $this->assertSame('domain-confirmed', $result['process'][4]['label']); + $this->assertSame(false, $result['process'][4]['state']); } // Test 'skus' property $user->assignSku(Sku::withEnvTenantContext()->where('title', 'beta')->first()); $result = UsersController::statusInfo($user); $this->assertSame(['beta'], $result['skus']); $this->assertTrue($result['enableBeta']); $user->assignSku(Sku::withEnvTenantContext()->where('title', 'groupware')->first()); $result = UsersController::statusInfo($user); $this->assertSame(['beta', 'groupware'], $result['skus']); // Degraded user $user->status |= User::STATUS_DEGRADED; $user->save(); $result = UsersController::statusInfo($user); $this->assertTrue($result['enableBeta']); $this->assertFalse($result['enableRooms']); // User in a tenant without 'room' SKU $user->status = User::STATUS_LDAP_READY | User::STATUS_IMAP_READY | User::STATUS_ACTIVE; $user->tenant_id = Tenant::where('title', 'Sample Tenant')->first()->id; $user->save(); $result = UsersController::statusInfo($user); $this->assertTrue($result['enableBeta']); $this->assertFalse($result['enableRooms']); } /** * Test user config update (POST /api/v4/users//config) */ public function testSetConfig(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $john->setSetting('greylist_enabled', null); $john->setSetting('guam_enabled', null); $john->setSetting('password_policy', null); $john->setSetting('max_password_age', null); // Test unknown user id $post = ['greylist_enabled' => 1]; $response = $this->actingAs($john)->post("/api/v4/users/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['greylist_enabled' => 1]; $response = $this->actingAs($jack)->post("/api/v4/users/{$john->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['grey' => 1, 'password_policy' => 'min:1,max:255']; $response = $this->actingAs($john)->post("/api/v4/users/{$john->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(2, $json['errors']); $this->assertSame("The requested configuration parameter is not supported.", $json['errors']['grey']); $this->assertSame("Minimum password length cannot be less than 6.", $json['errors']['password_policy']); $this->assertNull($john->fresh()->getSetting('greylist_enabled')); // Test some valid data $post = [ 'greylist_enabled' => 1, 'guam_enabled' => 1, 'password_policy' => 'min:10,max:255,upper,lower,digit,special', 'max_password_age' => 6, ]; $response = $this->actingAs($john)->post("/api/v4/users/{$john->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame('User settings updated successfully.', $json['message']); $this->assertSame('true', $john->getSetting('greylist_enabled')); $this->assertSame('true', $john->getSetting('guam_enabled')); $this->assertSame('min:10,max:255,upper,lower,digit,special', $john->getSetting('password_policy')); $this->assertSame('6', $john->getSetting('max_password_age')); // Test some valid data, acting as another account controller $ned = $this->getTestUser('ned@kolab.org'); $post = ['greylist_enabled' => 0, 'guam_enabled' => 0, 'password_policy' => 'min:10,max:255,upper,last:1']; $response = $this->actingAs($ned)->post("/api/v4/users/{$john->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame('User settings updated successfully.', $json['message']); $this->assertSame('false', $john->fresh()->getSetting('greylist_enabled')); $this->assertSame(null, $john->fresh()->getSetting('guam_enabled')); $this->assertSame('min:10,max:255,upper,last:1', $john->fresh()->getSetting('password_policy')); } /** * Test user creation (POST /api/v4/users) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $john->setSetting('password_policy', 'min:8,max:100,digit'); $deleted_priv = $this->getTestUser('deleted@kolab.org'); $deleted_priv->delete(); // Test empty request $response = $this->actingAs($john)->post("/api/v4/users", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The email field is required.", $json['errors']['email']); $this->assertSame("The password field is required.", $json['errors']['password'][0]); $this->assertCount(2, $json); // Test access by user not being a wallet controller $post = ['first_name' => 'Test']; $response = $this->actingAs($jack)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['password' => '12345678', 'email' => 'invalid']; $response = $this->actingAs($john)->post("/api/v4/users", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The password confirmation does not match.', $json['errors']['password'][0]); $this->assertSame('The specified email is invalid.', $json['errors']['email']); // Test existing user email $post = [ 'password' => 'simple123', 'password_confirmation' => 'simple123', 'first_name' => 'John2', 'last_name' => 'Doe2', 'email' => 'jack.daniels@kolab.org', ]; $response = $this->actingAs($john)->post("/api/v4/users", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified email is not available.', $json['errors']['email']); $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $post = [ 'password' => 'simple123', 'password_confirmation' => 'simple123', 'first_name' => 'John2', 'last_name' => 'Doe2', 'email' => 'john2.doe2@kolab.org', 'organization' => 'TestOrg', 'aliases' => ['useralias1@kolab.org', 'deleted@kolab.org'], ]; // Missing package $response = $this->actingAs($john)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Package is required.", $json['errors']['package']); $this->assertCount(2, $json); // Invalid package $post['package'] = $package_domain->id; $response = $this->actingAs($john)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Invalid package selected.", $json['errors']['package']); $this->assertCount(2, $json); // Test password policy checking $post['package'] = $package_kolab->id; $post['password'] = 'password'; $response = $this->actingAs($john)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]); $this->assertSame("Specified password does not comply with the policy.", $json['errors']['password'][1]); $this->assertCount(2, $json); // Test password confirmation $post['password_confirmation'] = 'password'; $response = $this->actingAs($john)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("Specified password does not comply with the policy.", $json['errors']['password'][0]); $this->assertCount(2, $json); // Test full and valid data $post['password'] = 'password123'; $post['password_confirmation'] = 'password123'; $response = $this->actingAs($john)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("User created successfully.", $json['message']); $this->assertCount(2, $json); $user = User::where('email', 'john2.doe2@kolab.org')->first(); $this->assertInstanceOf(User::class, $user); $this->assertSame('John2', $user->getSetting('first_name')); $this->assertSame('Doe2', $user->getSetting('last_name')); $this->assertSame('TestOrg', $user->getSetting('organization')); /** @var \App\UserAlias[] $aliases */ $aliases = $user->aliases()->orderBy('alias')->get(); $this->assertCount(2, $aliases); $this->assertSame('deleted@kolab.org', $aliases[0]->alias); $this->assertSame('useralias1@kolab.org', $aliases[1]->alias); // Assert the new user entitlements $this->assertEntitlements($user, ['groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage']); // Assert the wallet to which the new user should be assigned to $wallet = $user->wallet(); $this->assertSame($john->wallets->first()->id, $wallet->id); // Attempt to create a user previously deleted $user->delete(); $post['package'] = $package_kolab->id; $post['aliases'] = []; $response = $this->actingAs($john)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("User created successfully.", $json['message']); $this->assertCount(2, $json); $user = User::where('email', 'john2.doe2@kolab.org')->first(); $this->assertInstanceOf(User::class, $user); $this->assertSame('John2', $user->getSetting('first_name')); $this->assertSame('Doe2', $user->getSetting('last_name')); $this->assertSame('TestOrg', $user->getSetting('organization')); $this->assertCount(0, $user->aliases()->get()); $this->assertEntitlements($user, ['groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage']); // Test password reset link "mode" $code = new \App\VerificationCode(['mode' => 'password-reset', 'active' => false]); $john->verificationcodes()->save($code); $post = [ 'first_name' => 'John2', 'last_name' => 'Doe2', 'email' => 'deleted@kolab.org', 'organization' => '', 'aliases' => [], 'passwordLinkCode' => $code->short_code . '-' . $code->code, 'package' => $package_kolab->id, ]; $response = $this->actingAs($john)->post("/api/v4/users", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("User created successfully.", $json['message']); $this->assertCount(2, $json); $user = $this->getTestUser('deleted@kolab.org'); $code->refresh(); $this->assertSame($user->id, $code->user_id); $this->assertTrue($code->active); $this->assertTrue(is_string($user->password) && strlen($user->password) >= 60); // Test acting as account controller not owner, which is not yet supported $john->wallets->first()->addController($user); $response = $this->actingAs($user)->post("/api/v4/users", []); $response->assertStatus(403); } /** * Test user update (PUT /api/v4/users/) */ public function testUpdate(): void { $userA = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $userA->setSetting('password_policy', 'min:8,digit'); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $domain = $this->getTestDomain( 'userscontroller.com', ['status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL] ); // Test unauthorized update of other user profile $response = $this->actingAs($jack)->get("/api/v4/users/{$userA->id}", []); $response->assertStatus(403); // Test authorized update of account owner by account controller $response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}", []); $response->assertStatus(200); // Test updating of self (empty request) $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", []); $response->assertStatus(200); $json = $response->json(); $this->assertSame('success', $json['status']); $this->assertSame("User data updated successfully.", $json['message']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertCount(3, $json); // Test some invalid data $post = ['password' => '1234567', 'currency' => 'invalid']; $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]); $this->assertSame("Specified password does not comply with the policy.", $json['errors']['password'][1]); $this->assertSame("The currency must be 3 characters.", $json['errors']['currency'][0]); // Test full profile update including password $post = [ 'password' => 'simple123', 'password_confirmation' => 'simple123', 'first_name' => 'John2', 'last_name' => 'Doe2', 'organization' => 'TestOrg', 'phone' => '+123 123 123', 'external_email' => 'external@gmail.com', 'billing_address' => 'billing', 'country' => 'CH', 'currency' => 'CHF', 'aliases' => ['useralias1@' . \config('app.domain'), 'useralias2@' . \config('app.domain')] ]; $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("User data updated successfully.", $json['message']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertCount(3, $json); $this->assertTrue($userA->password != $userA->fresh()->password); unset($post['password'], $post['password_confirmation'], $post['aliases']); foreach ($post as $key => $value) { $this->assertSame($value, $userA->getSetting($key)); } $aliases = $userA->aliases()->orderBy('alias')->get(); $this->assertCount(2, $aliases); $this->assertSame('useralias1@' . \config('app.domain'), $aliases[0]->alias); $this->assertSame('useralias2@' . \config('app.domain'), $aliases[1]->alias); // Test unsetting values $post = [ 'first_name' => '', 'last_name' => '', 'organization' => '', 'phone' => '', 'external_email' => '', 'billing_address' => '', 'country' => '', 'currency' => '', 'aliases' => ['useralias2@' . \config('app.domain')] ]; $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("User data updated successfully.", $json['message']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertCount(3, $json); unset($post['aliases']); foreach ($post as $key => $value) { $this->assertNull($userA->getSetting($key)); } $aliases = $userA->aliases()->get(); $this->assertCount(1, $aliases); $this->assertSame('useralias2@' . \config('app.domain'), $aliases[0]->alias); // Test error on some invalid aliases missing password confirmation $post = [ 'password' => 'simple123', 'aliases' => [ 'useralias2@' . \config('app.domain'), 'useralias1@kolab.org', '@kolab.org', ] ]; $response = $this->actingAs($userA)->put("/api/v4/users/{$userA->id}", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertCount(2, $json['errors']['aliases']); $this->assertSame("The specified domain is not available.", $json['errors']['aliases'][1]); $this->assertSame("The specified alias is invalid.", $json['errors']['aliases'][2]); $this->assertSame("The password confirmation does not match.", $json['errors']['password'][0]); // Test authorized update of other user $response = $this->actingAs($ned)->put("/api/v4/users/{$jack->id}", []); $response->assertStatus(200); $json = $response->json(); $this->assertTrue(empty($json['statusInfo'])); // TODO: Test error on aliases with invalid/non-existing/other-user's domain // Create entitlements and additional user for following tests $owner = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $user = $this->getTestUser('UsersControllerTest2@userscontroller.com'); $package_domain = Package::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $package_kolab = Package::withEnvTenantContext()->where('title', 'kolab')->first(); $package_lite = Package::withEnvTenantContext()->where('title', 'lite')->first(); $sku_mailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); $sku_storage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); $sku_groupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first(); $domain = $this->getTestDomain( 'userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ] ); $domain->assignPackage($package_domain, $owner); $owner->assignPackage($package_kolab); $owner->assignPackage($package_lite, $user); // Non-controller cannot update his own entitlements $post = ['skus' => []]; $response = $this->actingAs($user)->put("/api/v4/users/{$user->id}", $post); $response->assertStatus(422); // Test updating entitlements $post = [ 'skus' => [ $sku_mailbox->id => 1, $sku_storage->id => 6, $sku_groupware->id => 1, ], ]; $response = $this->actingAs($owner)->put("/api/v4/users/{$user->id}", $post); $response->assertStatus(200); $json = $response->json(); $storage_cost = $user->entitlements() ->where('sku_id', $sku_storage->id) ->orderBy('cost') ->pluck('cost')->all(); $this->assertEntitlements( $user, ['groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage'] ); $this->assertSame([0, 0, 0, 0, 0, 25], $storage_cost); $this->assertTrue(empty($json['statusInfo'])); // Test password reset link "mode" $code = new \App\VerificationCode(['mode' => 'password-reset', 'active' => false]); $owner->verificationcodes()->save($code); $post = ['passwordLinkCode' => $code->short_code . '-' . $code->code]; $response = $this->actingAs($owner)->put("/api/v4/users/{$user->id}", $post); $json = $response->json(); $response->assertStatus(200); $code->refresh(); $this->assertSame($user->id, $code->user_id); $this->assertTrue($code->active); $this->assertSame($user->password, $user->fresh()->password); } /** * Test UsersController::updateEntitlements() */ public function testUpdateEntitlements(): void { $jane = $this->getTestUser('jane@kolabnow.com'); $kolab = Package::withEnvTenantContext()->where('title', 'kolab')->first(); $storage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); $activesync = Sku::withEnvTenantContext()->where('title', 'activesync')->first(); $groupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first(); $mailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); // standard package, 1 mailbox, 1 groupware, 2 storage $jane->assignPackage($kolab); // add 2 storage, 1 activesync $post = [ 'skus' => [ $mailbox->id => 1, $groupware->id => 1, $storage->id => 7, $activesync->id => 1 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(200); $this->assertEntitlements( $jane, [ 'activesync', 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage' ] ); // add 2 storage, remove 1 activesync $post = [ 'skus' => [ $mailbox->id => 1, $groupware->id => 1, $storage->id => 9, $activesync->id => 0 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(200); $this->assertEntitlements( $jane, [ 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage' ] ); // add mailbox $post = [ 'skus' => [ $mailbox->id => 2, $groupware->id => 1, $storage->id => 9, $activesync->id => 0 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(500); $this->assertEntitlements( $jane, [ 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage' ] ); // remove mailbox $post = [ 'skus' => [ $mailbox->id => 0, $groupware->id => 1, $storage->id => 9, $activesync->id => 0 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(500); $this->assertEntitlements( $jane, [ 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage' ] ); // less than free storage $post = [ 'skus' => [ $mailbox->id => 1, $groupware->id => 1, $storage->id => 1, $activesync->id => 0 ] ]; $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post); $response->assertStatus(200); $this->assertEntitlements( $jane, [ 'groupware', 'mailbox', 'storage', 'storage', 'storage', 'storage', 'storage' ] ); } /** * Test user data response used in show and info actions */ public function testUserResponse(): void { $provider = \config('services.payment_provider') ?: 'mollie'; $user = $this->getTestUser('john@kolab.org'); $wallet = $user->wallets()->first(); $wallet->setSettings(['mollie_id' => null, 'stripe_id' => null]); $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]); $this->assertEquals($user->id, $result['id']); $this->assertEquals($user->email, $result['email']); $this->assertEquals($user->status, $result['status']); $this->assertTrue(is_array($result['statusInfo'])); $this->assertTrue(is_array($result['settings'])); $this->assertSame('US', $result['settings']['country']); $this->assertSame('USD', $result['settings']['currency']); $this->assertTrue(is_array($result['accounts'])); $this->assertTrue(is_array($result['wallets'])); $this->assertCount(0, $result['accounts']); $this->assertCount(1, $result['wallets']); $this->assertSame($wallet->id, $result['wallet']['id']); $this->assertArrayNotHasKey('discount', $result['wallet']); $this->assertTrue($result['statusInfo']['enableDomains']); $this->assertTrue($result['statusInfo']['enableWallets']); $this->assertTrue($result['statusInfo']['enableUsers']); $this->assertTrue($result['statusInfo']['enableSettings']); // Ned is John's wallet controller $ned = $this->getTestUser('ned@kolab.org'); $ned_wallet = $ned->wallets()->first(); $result = $this->invokeMethod(new UsersController(), 'userResponse', [$ned]); $this->assertEquals($ned->id, $result['id']); $this->assertEquals($ned->email, $result['email']); $this->assertTrue(is_array($result['accounts'])); $this->assertTrue(is_array($result['wallets'])); $this->assertCount(1, $result['accounts']); $this->assertCount(1, $result['wallets']); $this->assertSame($wallet->id, $result['wallet']['id']); $this->assertSame($wallet->id, $result['accounts'][0]['id']); $this->assertSame($ned_wallet->id, $result['wallets'][0]['id']); $this->assertSame($provider, $result['wallet']['provider']); $this->assertSame($provider, $result['wallets'][0]['provider']); $this->assertTrue($result['statusInfo']['enableDomains']); $this->assertTrue($result['statusInfo']['enableWallets']); $this->assertTrue($result['statusInfo']['enableUsers']); $this->assertTrue($result['statusInfo']['enableSettings']); // Test discount in a response $discount = Discount::where('code', 'TEST')->first(); $wallet->discount()->associate($discount); $wallet->save(); $mod_provider = $provider == 'mollie' ? 'stripe' : 'mollie'; $wallet->setSetting($mod_provider . '_id', 123); $user->refresh(); $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]); $this->assertEquals($user->id, $result['id']); $this->assertSame($discount->id, $result['wallet']['discount_id']); $this->assertSame($discount->discount, $result['wallet']['discount']); $this->assertSame($discount->description, $result['wallet']['discount_description']); $this->assertSame($mod_provider, $result['wallet']['provider']); $this->assertSame($discount->id, $result['wallets'][0]['discount_id']); $this->assertSame($discount->discount, $result['wallets'][0]['discount']); $this->assertSame($discount->description, $result['wallets'][0]['discount_description']); $this->assertSame($mod_provider, $result['wallets'][0]['provider']); // Jack is not a John's wallet controller $jack = $this->getTestUser('jack@kolab.org'); $result = $this->invokeMethod(new UsersController(), 'userResponse', [$jack]); $this->assertFalse($result['statusInfo']['enableDomains']); $this->assertFalse($result['statusInfo']['enableWallets']); $this->assertFalse($result['statusInfo']['enableUsers']); $this->assertFalse($result['statusInfo']['enableSettings']); } /** * User email address validation. * * Note: Technically these include unit tests, but let's keep it here for now. * FIXME: Shall we do a http request for each case? */ public function testValidateEmail(): void { Queue::fake(); $public_domains = Domain::getPublicDomains(); $domain = reset($public_domains); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $folder = $this->getTestSharedFolder('folder-event@kolab.org'); $folder->setAliases(['folder-alias1@kolab.org']); $folder_del = $this->getTestSharedFolder('folder-test@kolabnow.com'); $folder_del->setAliases(['folder-alias2@kolabnow.com']); $folder_del->delete(); $pub_group = $this->getTestGroup('group-test@kolabnow.com'); $pub_group->delete(); $priv_group = $this->getTestGroup('group-test@kolab.org'); $resource = $this->getTestResource('resource-test@kolabnow.com'); $resource->delete(); $cases = [ // valid (user domain) ["admin@kolab.org", $john, null], // valid (public domain) ["test.test@$domain", $john, null], // Invalid format ["$domain", $john, 'The specified email is invalid.'], [".@$domain", $john, 'The specified email is invalid.'], ["test123456@localhost", $john, 'The specified domain is invalid.'], ["test123456@unknown-domain.org", $john, 'The specified domain is invalid.'], ["$domain", $john, 'The specified email is invalid.'], [".@$domain", $john, 'The specified email is invalid.'], // forbidden local part on public domains ["admin@$domain", $john, 'The specified email is not available.'], ["administrator@$domain", $john, 'The specified email is not available.'], // forbidden (other user's domain) ["testtest@kolab.org", $user, 'The specified domain is not available.'], // existing alias of other user ["jack.daniels@kolab.org", $john, 'The specified email is not available.'], // An existing shared folder or folder alias ["folder-event@kolab.org", $john, 'The specified email is not available.'], ["folder-alias1@kolab.org", $john, 'The specified email is not available.'], // A soft-deleted shared folder or folder alias ["folder-test@kolabnow.com", $john, 'The specified email is not available.'], ["folder-alias2@kolabnow.com", $john, 'The specified email is not available.'], // A group ["group-test@kolab.org", $john, 'The specified email is not available.'], // A soft-deleted group ["group-test@kolabnow.com", $john, 'The specified email is not available.'], // A resource ["resource-test1@kolab.org", $john, 'The specified email is not available.'], // A soft-deleted resource ["resource-test@kolabnow.com", $john, 'The specified email is not available.'], ]; foreach ($cases as $idx => $case) { list($email, $user, $expected) = $case; $deleted = null; $result = UsersController::validateEmail($email, $user, $deleted); $this->assertSame($expected, $result, "Case {$email}"); $this->assertNull($deleted, "Case {$email}"); } } /** * User email validation - tests for $deleted argument * * Note: Technically these include unit tests, but let's keep it here for now. * FIXME: Shall we do a http request for each case? */ public function testValidateEmailDeleted(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $deleted_priv = $this->getTestUser('deleted@kolab.org'); $deleted_priv->delete(); $deleted_pub = $this->getTestUser('deleted@kolabnow.com'); $deleted_pub->delete(); $result = UsersController::validateEmail('deleted@kolab.org', $john, $deleted); $this->assertSame(null, $result); $this->assertSame($deleted_priv->id, $deleted->id); $result = UsersController::validateEmail('deleted@kolabnow.com', $john, $deleted); $this->assertSame('The specified email is not available.', $result); $this->assertSame(null, $deleted); $result = UsersController::validateEmail('jack@kolab.org', $john, $deleted); $this->assertSame('The specified email is not available.', $result); $this->assertSame(null, $deleted); $pub_group = $this->getTestGroup('group-test@kolabnow.com'); $priv_group = $this->getTestGroup('group-test@kolab.org'); // A group in a public domain, existing $result = UsersController::validateEmail($pub_group->email, $john, $deleted); $this->assertSame('The specified email is not available.', $result); $this->assertNull($deleted); $pub_group->delete(); // A group in a public domain, deleted $result = UsersController::validateEmail($pub_group->email, $john, $deleted); $this->assertSame('The specified email is not available.', $result); $this->assertNull($deleted); // A group in a private domain, existing $result = UsersController::validateEmail($priv_group->email, $john, $deleted); $this->assertSame('The specified email is not available.', $result); $this->assertNull($deleted); $priv_group->delete(); // A group in a private domain, deleted $result = UsersController::validateEmail($priv_group->email, $john, $deleted); $this->assertSame(null, $result); $this->assertSame($priv_group->id, $deleted->id); // TODO: Test the same with a resource and shared folder } /** * User email alias validation. * * Note: Technically these include unit tests, but let's keep it here for now. * FIXME: Shall we do a http request for each case? */ public function testValidateAlias(): void { Queue::fake(); $public_domains = Domain::getPublicDomains(); $domain = reset($public_domains); $john = $this->getTestUser('john@kolab.org'); $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $deleted_priv = $this->getTestUser('deleted@kolab.org'); $deleted_priv->setAliases(['deleted-alias@kolab.org']); $deleted_priv->delete(); $deleted_pub = $this->getTestUser('deleted@kolabnow.com'); $deleted_pub->setAliases(['deleted-alias@kolabnow.com']); $deleted_pub->delete(); $folder = $this->getTestSharedFolder('folder-event@kolab.org'); $folder->setAliases(['folder-alias1@kolab.org']); $folder_del = $this->getTestSharedFolder('folder-test@kolabnow.com'); $folder_del->setAliases(['folder-alias2@kolabnow.com']); $folder_del->delete(); $group_priv = $this->getTestGroup('group-test@kolab.org'); $group = $this->getTestGroup('group-test@kolabnow.com'); $group->delete(); $resource = $this->getTestResource('resource-test@kolabnow.com'); $resource->delete(); $cases = [ // Invalid format ["$domain", $john, 'The specified alias is invalid.'], [".@$domain", $john, 'The specified alias is invalid.'], ["test123456@localhost", $john, 'The specified domain is invalid.'], ["test123456@unknown-domain.org", $john, 'The specified domain is invalid.'], ["$domain", $john, 'The specified alias is invalid.'], [".@$domain", $john, 'The specified alias is invalid.'], // forbidden local part on public domains ["admin@$domain", $john, 'The specified alias is not available.'], ["administrator@$domain", $john, 'The specified alias is not available.'], // forbidden (other user's domain) ["testtest@kolab.org", $user, 'The specified domain is not available.'], // existing alias of other user, to be an alias, user in the same group account ["jack.daniels@kolab.org", $john, null], // existing user ["jack@kolab.org", $john, 'The specified alias is not available.'], // valid (user domain) ["admin@kolab.org", $john, null], // valid (public domain) ["test.test@$domain", $john, null], // An alias that was a user email before is allowed, but only for custom domains ["deleted@kolab.org", $john, null], ["deleted-alias@kolab.org", $john, null], ["deleted@kolabnow.com", $john, 'The specified alias is not available.'], ["deleted-alias@kolabnow.com", $john, 'The specified alias is not available.'], // An existing shared folder or folder alias ["folder-event@kolab.org", $john, 'The specified alias is not available.'], ["folder-alias1@kolab.org", $john, null], // A soft-deleted shared folder or folder alias ["folder-test@kolabnow.com", $john, 'The specified alias is not available.'], ["folder-alias2@kolabnow.com", $john, 'The specified alias is not available.'], // A group with the same email address exists ["group-test@kolab.org", $john, 'The specified alias is not available.'], // A soft-deleted group ["group-test@kolabnow.com", $john, 'The specified alias is not available.'], // A resource ["resource-test1@kolab.org", $john, 'The specified alias is not available.'], // A soft-deleted resource ["resource-test@kolabnow.com", $john, 'The specified alias is not available.'], ]; foreach ($cases as $idx => $case) { list($alias, $user, $expected) = $case; $result = UsersController::validateAlias($alias, $user); $this->assertSame($expected, $result, "Case {$alias}"); } } } diff --git a/src/tests/Feature/Jobs/Resource/CreateTest.php b/src/tests/Feature/Jobs/Resource/CreateTest.php index 3da1cd8b..12ff7fb1 100644 --- a/src/tests/Feature/Jobs/Resource/CreateTest.php +++ b/src/tests/Feature/Jobs/Resource/CreateTest.php @@ -1,89 +1,89 @@ deleteTestResource('resource-test@' . \config('app.domain')); } public function tearDown(): void { $this->deleteTestResource('resource-test@' . \config('app.domain')); parent::tearDown(); } /** * Test job handle * * @group ldap * @group imap */ public function testHandle(): void { Queue::fake(); // Test unknown resource $this->expectException(\Exception::class); $job = new \App\Jobs\Resource\CreateJob(123); $job->handle(); $this->assertTrue($job->isReleased()); $this->assertFalse($job->hasFailed()); $resource = $this->getTestResource( 'resource-test@' . \config('app.domain'), ['status' => Resource::STATUS_NEW] ); $this->assertFalse($resource->isLdapReady()); $this->assertFalse($resource->isImapReady()); $this->assertFalse($resource->isActive()); // Test resource creation $job = new \App\Jobs\Resource\CreateJob($resource->id); $job->handle(); $resource->refresh(); $this->assertFalse($job->hasFailed()); - $this->assertTrue($resource->isLdapReady()); + $this->assertSame(\config('app.with_ldap'), $resource->isLdapReady()); $this->assertTrue($resource->isImapReady()); $this->assertTrue($resource->isActive()); // Test job failures $resource->status |= Resource::STATUS_DELETED; $resource->save(); $job = new \App\Jobs\Resource\CreateJob($resource->id); $job->handle(); $this->assertTrue($job->hasFailed()); $this->assertSame("Resource {$resource->id} is marked as deleted.", $job->failureMessage); $resource->status ^= Resource::STATUS_DELETED; $resource->save(); $resource->delete(); $job = new \App\Jobs\Resource\CreateJob($resource->id); $job->handle(); $this->assertTrue($job->hasFailed()); $this->assertSame("Resource {$resource->id} is actually deleted.", $job->failureMessage); // TODO: Test failures on domain sanity checks // TODO: Test partial execution, i.e. only IMAP or only LDAP } } diff --git a/src/tests/Feature/Jobs/Resource/DeleteTest.php b/src/tests/Feature/Jobs/Resource/DeleteTest.php index 4f88a862..7a7bea41 100644 --- a/src/tests/Feature/Jobs/Resource/DeleteTest.php +++ b/src/tests/Feature/Jobs/Resource/DeleteTest.php @@ -1,78 +1,76 @@ deleteTestResource('resource-test@' . \config('app.domain')); } public function tearDown(): void { $this->deleteTestResource('resource-test@' . \config('app.domain')); parent::tearDown(); } /** * Test job handle * * @group ldap * @group imap */ public function testHandle(): void { Queue::fake(); // Test non-existing resource ID $job = new \App\Jobs\Resource\DeleteJob(123); $job->handle(); $this->assertTrue($job->hasFailed()); $this->assertSame("Resource 123 could not be found in the database.", $job->failureMessage); $resource = $this->getTestResource('resource-test@' . \config('app.domain'), [ 'status' => Resource::STATUS_NEW ]); // create the resource first $job = new \App\Jobs\Resource\CreateJob($resource->id); $job->handle(); $resource->refresh(); - $this->assertTrue($resource->isLdapReady()); - if (\config('app.with_imap')) { - $this->assertTrue($resource->isImapReady()); - } + $this->assertSame(\config('app.with_ldap'), $resource->isLdapReady()); + $this->assertTrue($resource->isImapReady()); $this->assertFalse($resource->isDeleted()); // Test successful deletion $job = new \App\Jobs\Resource\DeleteJob($resource->id); $job->handle(); $resource->refresh(); $this->assertFalse($resource->isLdapReady()); $this->assertFalse($resource->isImapReady()); $this->assertTrue($resource->isDeleted()); // Test deleting already deleted resource $job = new \App\Jobs\Resource\DeleteJob($resource->id); $job->handle(); $this->assertTrue($job->hasFailed()); $this->assertSame("Resource {$resource->id} is already marked as deleted.", $job->failureMessage); } } diff --git a/src/tests/Feature/Jobs/SharedFolder/DeleteTest.php b/src/tests/Feature/Jobs/SharedFolder/DeleteTest.php index 0a87b39e..8334e044 100644 --- a/src/tests/Feature/Jobs/SharedFolder/DeleteTest.php +++ b/src/tests/Feature/Jobs/SharedFolder/DeleteTest.php @@ -1,80 +1,80 @@ deleteTestSharedFolder('folder-test@' . \config('app.domain')); } public function tearDown(): void { $this->deleteTestSharedFolder('folder-test@' . \config('app.domain')); parent::tearDown(); } /** * Test job handle * * @group ldap * @group imap */ public function testHandle(): void { Queue::fake(); // Test non-existing folder ID $job = new \App\Jobs\SharedFolder\DeleteJob(123); $job->handle(); $this->assertTrue($job->hasFailed()); $this->assertSame("Shared folder 123 could not be found in the database.", $job->failureMessage); $folder = $this->getTestSharedFolder('folder-test@' . \config('app.domain'), [ 'status' => SharedFolder::STATUS_NEW ]); + // Force with_imap=true, otherwise the folder creation job may fail + // TODO: Make the test working with various with_imap/with_ldap combinations + \config(['app.with_imap' => true]); + // create the shared folder first $job = new \App\Jobs\SharedFolder\CreateJob($folder->id); $job->handle(); $folder->refresh(); - $this->assertTrue($folder->isLdapReady()); - if (\config('app.with_imap')) { - $this->assertTrue($folder->isImapReady()); - } + $this->assertSame(\config('app.with_ldap'), $folder->isLdapReady()); + $this->assertTrue($folder->isImapReady()); $this->assertFalse($folder->isDeleted()); // Test successful deletion $job = new \App\Jobs\SharedFolder\DeleteJob($folder->id); $job->handle(); $folder->refresh(); $this->assertFalse($folder->isLdapReady()); - if (\config('app.with_imap')) { - $this->assertFalse($folder->isImapReady()); - } + $this->assertFalse($folder->isImapReady()); $this->assertTrue($folder->isDeleted()); // Test deleting already deleted folder $job = new \App\Jobs\SharedFolder\DeleteJob($folder->id); $job->handle(); $this->assertTrue($job->hasFailed()); $this->assertSame("Shared folder {$folder->id} is already marked as deleted.", $job->failureMessage); } } diff --git a/src/tests/Feature/Jobs/SharedFolder/UpdateTest.php b/src/tests/Feature/Jobs/SharedFolder/UpdateTest.php index 1d4c8b97..ea9af1ef 100644 --- a/src/tests/Feature/Jobs/SharedFolder/UpdateTest.php +++ b/src/tests/Feature/Jobs/SharedFolder/UpdateTest.php @@ -1,80 +1,82 @@ deleteTestSharedFolder('folder-test@' . \config('app.domain')); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestSharedFolder('folder-test@' . \config('app.domain')); parent::tearDown(); } /** * Test job handle * * @group ldap * @group imap */ public function testHandle(): void { Queue::fake(); // Test non-existing folder ID $job = new \App\Jobs\SharedFolder\UpdateJob(123); $job->handle(); $this->assertTrue($job->hasFailed()); $this->assertSame("Shared folder 123 could not be found in the database.", $job->failureMessage); $folder = $this->getTestSharedFolder( 'folder-test@' . \config('app.domain'), ['status' => SharedFolder::STATUS_NEW] ); - // Create the folder in LDAP + // Force with_imap=true, otherwise the folder creation job may fail + // TODO: Make the test working with various with_imap=false + \config(['app.with_imap' => true]); + + // Create the folder in LDAP/IMAP $job = new \App\Jobs\SharedFolder\CreateJob($folder->id); $job->handle(); $folder->refresh(); - $this->assertTrue($folder->isLdapReady()); - if (\config('app.with_imap')) { - $this->assertTrue($folder->isImapReady()); - } + $this->assertSame(\config('app.with_ldap'), $folder->isLdapReady()); + $this->assertTrue($folder->isImapReady()); // Run the update job $job = new \App\Jobs\SharedFolder\UpdateJob($folder->id); $job->handle(); // TODO: Assert that it worked on both LDAP and IMAP side // Test handling deleted folder $folder->status |= SharedFolder::STATUS_DELETED; $folder->save(); $job = new \App\Jobs\SharedFolder\UpdateJob($folder->id); $job->handle(); $this->assertTrue($job->isDeleted()); } } diff --git a/src/tests/Feature/Jobs/User/DeleteTest.php b/src/tests/Feature/Jobs/User/DeleteTest.php index b4d2bcb8..ba780e76 100644 --- a/src/tests/Feature/Jobs/User/DeleteTest.php +++ b/src/tests/Feature/Jobs/User/DeleteTest.php @@ -1,100 +1,100 @@ deleteTestUser('new-job-user@' . \config('app.domain')); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestUser('new-job-user@' . \config('app.domain')); parent::tearDown(); } /** * Test job handle * * @group ldap * @group imap * @group roundcube */ public function testHandle(): void { Queue::fake(); $rcdb = Roundcube::dbh(); $user = $this->getTestUser('new-job-user@' . \config('app.domain')); $rcuser = Roundcube::userId($user->email); // Create the user in LDAP+IMAP $job = new \App\Jobs\User\CreateJob($user->id); $job->handle(); $user->refresh(); - $this->assertTrue($user->isLdapReady()); + $this->assertSame(\config('app.with_ldap'), $user->isLdapReady()); $this->assertTrue($user->isImapReady()); $this->assertFalse($user->isDeleted()); $this->assertNotNull($rcdb->table('users')->where('username', $user->email)->first()); // Test job failure (user already deleted) $user->status |= User::STATUS_DELETED; $user->save(); $job = new \App\Jobs\User\DeleteJob($user->id); $job->handle(); $this->assertTrue($job->hasFailed()); $this->assertSame("User {$user->id} is already marked as deleted.", $job->failureMessage); // Test success delete from LDAP, IMAP and Roundcube $user->status ^= User::STATUS_DELETED; $user->save(); $this->assertFalse($user->isDeleted()); $job = new \App\Jobs\User\DeleteJob($user->id); $job->handle(); $user->refresh(); $this->assertFalse($job->hasFailed()); $this->assertFalse($user->isLdapReady()); $this->assertFalse($user->isImapReady()); $this->assertTrue($user->isDeleted()); $this->assertNull($rcdb->table('users')->where('username', $user->email)->first()); if (\config('app.with_imap')) { Queue::assertPushed(\App\Jobs\IMAP\AclCleanupJob::class, 1); Queue::assertPushed( \App\Jobs\IMAP\AclCleanupJob::class, function ($job) use ($user) { $ident = TestCase::getObjectProperty($job, 'ident'); $domain = TestCase::getObjectProperty($job, 'domain'); return $ident == $user->email && $domain === ''; } ); } // TODO: Test partial execution, i.e. only IMAP or only LDAP } }