Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
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 @@
<?php
namespace App\Http\Controllers\API\V4;
use App\Http\Controllers\RelationController;
use App\Domain;
use App\Rules\Password;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\Sku;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
class UsersController extends RelationController
{
/** @const array List of user setting keys available for modification in UI */
public const USER_SETTINGS = [
'billing_address',
'country',
'currency',
'external_email',
'first_name',
'last_name',
'organization',
'phone',
];
/**
* On user create it is filled with a user or group object to force-delete
* before the creation of a new user record is possible.
*
* @var \App\User|\App\Group|null
*/
protected $deleteBeforeCreate;
/** @var string Resource localization label */
protected $label = 'user';
/** @var string Resource model name */
protected $model = User::class;
/** @var array Common object properties in the API response */
protected $objectProps = ['email'];
/** @var ?\App\VerificationCode Password reset code to activate on user create/update */
protected $passCode;
/**
* Listing of users.
*
* The user-entitlements billed to the current user wallet(s)
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->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 <short-code>-<code> 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 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Str;
class RelationController extends ResourceController
{
/** @var array Common object properties in the API response */
protected $objectProps = [];
/** @var string Resource localization label */
protected $label = '';
/** @var string Resource model name */
protected $model = '';
/** @var array Resource listing order (column names) */
protected $order = [];
/** @var array Resource relation method arguments */
protected $relationArgs = [];
/**
* Delete a resource.
*
* @param string $id Resource identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function destroy($id)
{
$resource = $this->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 @@
<?php
namespace App\Jobs\Resource;
use App\Jobs\ResourceJob;
class CreateJob extends ResourceJob
{
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$resource = $this->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 @@
<?php
namespace App\Jobs\Resource;
use App\Jobs\ResourceJob;
class DeleteJob extends ResourceJob
{
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$resource = $this->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 @@
<?php
namespace App\Jobs\SharedFolder;
use App\Jobs\SharedFolderJob;
class CreateJob extends SharedFolderJob
{
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$folder = $this->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 @@
<?php
namespace App\Jobs\SharedFolder;
use App\Jobs\SharedFolderJob;
class DeleteJob extends SharedFolderJob
{
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$folder = $this->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 @@
<?php
namespace App\Jobs\User;
use App\Jobs\UserJob;
/**
* Create the \App\User in LDAP.
*
* Throws exceptions for the following reasons:
*
* * The user is marked as deleted (`$user->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 @@
<?php
namespace App\Observers;
use App\SignupCode;
use Carbon\Carbon;
use Illuminate\Support\Str;
class SignupCodeObserver
{
/**
* Handle the "creating" event.
*
* Ensure that the code entry is created with a random code/short_code.
*
* @param \App\SignupCode $code The code being created.
*
* @return void
*/
public function creating(SignupCode $code): void
{
$code_length = SignupCode::CODE_LENGTH;
$exp_hours = env('SIGNUP_CODE_EXPIRY', SignupCode::CODE_EXP_HOURS);
if (empty($code->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 @@
<?php
namespace App\Traits;
trait EmailPropertyTrait
{
/** @var ?string Domain name for the to-be-created object */
public $domainName;
/**
* Boot function from Laravel.
*/
protected static function bootEmailPropertyTrait()
{
static::creating(function ($model) {
if (empty($model->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 @@
<template>
- <div v-if="!state.isReady" id="status-box" :class="'p-4 mb-3 rounded process-' + className">
+ <div v-if="!state.isDone" id="status-box" :class="'p-4 mb-3 rounded process-' + className">
<div v-if="state.step != 'domain-confirmed'" class="d-flex align-items-start">
<p id="status-body" class="flex-grow-1">
<span>{{ $t('status.prepare-' + scopeLabel()) }}</span>
<br>
{{ $t('status.prepare-hint') }}
<br>
<span id="refresh-text" v-if="refresh">{{ $t('status.prepare-refresh') }}</span>
</p>
<btn v-if="refresh" id="status-refresh" href="#" class="btn-secondary" @click="statusRefresh" icon="rotate">
{{ $t('btn.refresh') }}
</btn>
</div>
<div v-else class="d-flex align-items-start">
<p id="status-body" class="flex-grow-1">
<span>{{ $t('status.ready-' + scopeLabel()) }}</span>
<br>
{{ $t('status.verify') }}
</p>
<div v-if="scope == 'domain'">
<btn id="status-verify" class="btn-secondary text-nowrap" @click="confirmDomain" icon="rotate">
{{ $t('btn.verify') }}
</btn>
</div>
<div v-else-if="state.link && scope != 'domain'">
<router-link id="status-link" class="btn btn-secondary" :to="{ path: state.link }">{{ $t('status.verify-domain') }}</router-link>
</div>
</div>
<div class="status-progress text-center">
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<span class="progress-label">{{ state.title || $t('msg.initializing') }}</span>
</div>
</div>
</template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
require('@fortawesome/free-solid-svg-icons/faRotate').definition,
)
export default {
props: {
status: { type: Object, default: () => {} }
},
data() {
return {
className: 'pending',
refresh: false,
delay: 5000,
scope: 'user',
- state: { isReady: true },
+ state: { isDone: true },
waiting: 0,
}
},
watch: {
// We use property watcher because parent component
// might set the property with a delay and we need to parse it
// FIXME: Problem with this and update-status event is that whenever
// we emit the event a watcher function is executed, causing
// duplicate parseStatusInfo() calls. Fortunaltely this does not
// cause duplicate http requests.
status: function (val, oldVal) {
this.parseStatusInfo(val)
}
},
destroyed() {
clearTimeout(window.infoRequest)
},
mounted() {
this.scope = this.$route.name
},
methods: {
// Displays account status information
parseStatusInfo(info) {
if (info) {
- if (!info.isReady) {
+ if (!info.isDone) {
let failedCount = 0
let allCount = info.process.length
info.process.forEach((step, idx) => {
if (!step.state) {
failedCount++
if (!info.title) {
info.title = step.title
info.step = step.label
info.link = step.link
}
}
})
info.percent = Math.floor((allCount - failedCount) / allCount * 100);
}
this.state = info || {}
this.$nextTick(function() {
$(this.$el).find('.progress-bar')
.css('width', info.percent + '%')
.attr('aria-valuenow', info.percent)
})
// Unhide the Refresh button, the process is in failure state
this.refresh = info.processState == 'failed' && this.waiting == 0
if (this.refresh || info.step == 'domain-confirmed') {
this.className = 'failed'
}
// A async job has been dispatched, switch to a waiting mode where
// we hide the Refresh button and pull status for about a minute,
// after that we switch to normal mode, i.e. user can Refresh again (if still not ready)
if (info.processState == 'waiting') {
this.waiting = 10
this.delay = 5000
} else if (this.waiting > 0) {
this.waiting -= 1
}
}
// Update status process info every 5,6,7,8,9,... seconds
clearTimeout(window.infoRequest)
- if ((!this.refresh || this.waiting > 0) && (!info || !info.isReady)) {
+ if ((!this.refresh || this.waiting > 0) && (!info || !info.isDone)) {
window.infoRequest = setTimeout(() => {
delete window.infoRequest
// Stop updates after user logged out
if (!this.$root.authInfo) {
return;
}
axios.get(this.getUrl())
.then(response => {
this.parseStatusInfo(response.data)
this.emitEvent(response.data)
})
.catch(error => {
this.parseStatusInfo(info)
})
}, this.delay);
this.delay += 1000;
}
},
statusRefresh() {
clearTimeout(window.infoRequest)
axios.get(this.getUrl() + '?refresh=1')
.then(response => {
this.$toast[response.data.status](response.data.message)
this.parseStatusInfo(response.data)
this.emitEvent(response.data)
})
.catch(error => {
this.parseStatusInfo(this.state)
})
},
confirmDomain() {
axios.get('/api/v4/domains/' + this.$route.params.domain + '/confirm')
.then(response => {
if (response.data.message) {
this.$toast[response.data.status](response.data.message)
}
if (response.data.status == 'success') {
this.parseStatusInfo(response.data.statusInfo)
response.data.isConfirmed = true
this.emitEvent(response.data)
}
})
},
emitEvent(data) {
// Remove useless data and emit the event (to parent components)
delete data.status
delete data.message
this.$emit('status-update', data)
},
getUrl() {
let scope = this.scope
let id = this.$route.params[scope]
if (scope == 'dashboard') {
id = this.$root.authInfo.id
scope = 'user'
} else if (scope =='distlist') {
id = this.$route.params.list
scope = 'group'
} else if (scope == 'shared-folder') {
id = this.$route.params.folder
}
return '/api/v4/' + scope + 's/' + id + '/status'
},
scopeLabel() {
return this.scope == 'dashboard' ? 'account' : this.scope
}
}
}
</script>
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 @@
<?php
namespace Tests\Browser;
use App\Domain;
use App\User;
use Carbon\Carbon;
use Tests\Browser;
use Tests\Browser\Components\Status;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\DomainInfo;
use Tests\Browser\Pages\DomainList;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\UserInfo;
use Tests\Browser\Pages\UserList;
use Tests\TestCaseDusk;
use Illuminate\Support\Facades\DB;
class StatusTest extends TestCaseDusk
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$domain_status = Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED;
DB::statement("UPDATE domains SET status = (status | {$domain_status})"
. " WHERE namespace = 'kolab.org'");
- DB::statement("UPDATE users SET status = (status | " . User::STATUS_IMAP_READY . ")"
+ DB::statement("UPDATE users SET status = (status | " . (User::STATUS_IMAP_READY | User::STATUS_LDAP_READY) . ")"
. " WHERE email = 'john@kolab.org'");
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$domain_status = Domain::STATUS_CONFIRMED | Domain::STATUS_VERIFIED;
DB::statement("UPDATE domains SET status = (status | {$domain_status})"
. " WHERE namespace = 'kolab.org'");
DB::statement("UPDATE users SET status = (status | " . User::STATUS_IMAP_READY . ")"
. " WHERE email = 'john@kolab.org'");
parent::tearDown();
}
/**
* Test account status in the Dashboard
*/
public function testDashboard(): void
{
// Unconfirmed domain and user
$domain = Domain::where('namespace', 'kolab.org')->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 @@
<?php
namespace Tests\Feature\Controller;
use App\Domain;
use App\Entitlement;
use App\Sku;
use App\Tenant;
use App\User;
use App\Wallet;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
class DomainsTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$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();
}
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/<id>)
*/
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/<domain>/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/<id>/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/<domain-id>/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 @@
<?php
namespace Tests\Feature\Controller;
use App\Group;
use App\Http\Controllers\API\V4\GroupsController;
use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class GroupsTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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/<id>)
*/
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/<group>/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/<group-id>)
*/
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/<id>/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/<group-id>/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/<group-id>)
*/
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 @@
<?php
namespace Tests\Feature\Controller;
use App\Resource;
use App\Http\Controllers\API\V4\ResourcesController;
use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class ResourcesTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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/<id>)
*/
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/<resource>/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/<resource>)
*/
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/<id>/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/<resource>/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/<resource>)
*/
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 @@
<?php
namespace Tests\Feature\Controller;
use App\SharedFolder;
use App\Http\Controllers\API\V4\SharedFoldersController;
use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class SharedFoldersTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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/<id>)
*/
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/<folder>/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/<folder>)
*/
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/<id>/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/<folder>/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/<folder)
*/
public function testUpdate(): void
{
Queue::fake();
$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());
// 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 @@
<?php
namespace Tests\Feature\Controller;
use App\Discount;
use App\Domain;
use App\Http\Controllers\API\V4\UsersController;
use App\Package;
use App\Sku;
use App\Tenant;
use App\User;
use App\Wallet;
use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
class UsersTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$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();
}
/**
* {@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/<id>)
*/
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/<id>)
*/
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/<user-id>)
*/
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/<id>/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/<user-id>/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/<user>/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/<user-id>)
*/
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 @@
<?php
namespace Tests\Feature\Jobs\Resource;
use App\Resource;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class CreateTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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 @@
<?php
namespace Tests\Feature\Jobs\Resource;
use App\Resource;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class DeleteTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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 @@
<?php
namespace Tests\Feature\Jobs\SharedFolder;
use App\SharedFolder;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class DeleteTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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 @@
<?php
namespace Tests\Feature\Jobs\SharedFolder;
use App\Backends\LDAP;
use App\SharedFolder;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class UpdateTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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 @@
<?php
namespace Tests\Feature\Jobs\User;
use App\Backends\Roundcube;
use App\User;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class DeleteTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->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
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Jun 28, 1:15 PM (13 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
201439
Default Alt Text
(259 KB)

Event Timeline