diff --git a/src/app/Http/Controllers/API/V4/Admin/SharedFoldersController.php b/src/app/Http/Controllers/API/V4/Admin/SharedFoldersController.php index 88eacb53..2bf4e2a2 100644 --- a/src/app/Http/Controllers/API/V4/Admin/SharedFoldersController.php +++ b/src/app/Http/Controllers/API/V4/Admin/SharedFoldersController.php @@ -1,59 +1,59 @@ input('search')); $owner = trim(request()->input('owner')); $result = collect([]); if ($owner) { if ($owner = User::find($owner)) { $result = $owner->sharedFolders(false)->orderBy('name')->get(); } } elseif (!empty($search)) { if ($folder = SharedFolder::where('email', $search)->first()) { $result->push($folder); } } // Process the result $result = $result->map( function ($folder) { return $this->objectToClient($folder); } ); $result = [ 'list' => $result, 'count' => count($result), - 'message' => \trans('app.search-foundxsharedfolders', ['x' => count($result)]), + 'message' => \trans('app.search-foundxshared-folders', ['x' => count($result)]), ]; return response()->json($result); } /** * Create a new shared folder. * * @param \Illuminate\Http\Request $request The API request. * * @return \Illuminate\Http\JsonResponse The response */ public function store(Request $request) { return $this->errorResponse(404); } } diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php index fc8b521e..b028f132 100644 --- a/src/app/Http/Controllers/RelationController.php +++ b/src/app/Http/Controllers/RelationController.php @@ -1,354 +1,363 @@ 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}(); } } 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(); } 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 = []; // Create a process check list foreach ($steps as $step_name => $state) { $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, ]; } /** * 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']; $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(); } return response()->json($response); } /** * 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/resources/lang/en/app.php b/src/resources/lang/en/app.php index c3c8e650..3da22d53 100644 --- a/src/resources/lang/en/app.php +++ b/src/resources/lang/en/app.php @@ -1,135 +1,135 @@ 'Created', 'chart-deleted' => 'Deleted', 'chart-average' => 'average', 'chart-allusers' => 'All Users - last year', 'chart-discounts' => 'Discounts', 'chart-vouchers' => 'Vouchers', 'chart-income' => 'Income in :currency - last 8 weeks', 'chart-users' => 'Users - last 8 weeks', 'companion-deleteall-success' => 'All companion apps have been removed.', 'mandate-delete-success' => 'The auto-payment has been removed.', 'mandate-update-success' => 'The auto-payment has been updated.', 'planbutton' => 'Choose :plan', 'process-async' => 'Setup process has been pushed. Please wait.', 'process-user-new' => 'Registering a user...', 'process-user-ldap-ready' => 'Creating a user...', 'process-user-imap-ready' => 'Creating a mailbox...', 'process-domain-new' => 'Registering a custom domain...', 'process-domain-ldap-ready' => 'Creating a custom domain...', 'process-domain-verified' => 'Verifying a custom domain...', 'process-domain-confirmed' => 'Verifying an ownership of a custom domain...', 'process-success' => 'Setup process finished successfully.', 'process-error-distlist-ldap-ready' => 'Failed to create a distribution list.', 'process-error-domain-ldap-ready' => 'Failed to create a domain.', 'process-error-domain-verified' => 'Failed to verify a domain.', 'process-error-domain-confirmed' => 'Failed to verify an ownership of a domain.', 'process-error-resource-imap-ready' => 'Failed to verify that a shared folder exists.', 'process-error-resource-ldap-ready' => 'Failed to create a resource.', 'process-error-shared-folder-imap-ready' => 'Failed to verify that a shared folder exists.', 'process-error-shared-folder-ldap-ready' => 'Failed to create a shared folder.', 'process-error-user-ldap-ready' => 'Failed to create a user.', 'process-error-user-imap-ready' => 'Failed to verify that a mailbox exists.', 'process-distlist-new' => 'Registering a distribution list...', 'process-distlist-ldap-ready' => 'Creating a distribution list...', 'process-resource-new' => 'Registering a resource...', 'process-resource-imap-ready' => 'Creating a shared folder...', 'process-resource-ldap-ready' => 'Creating a resource...', 'process-shared-folder-new' => 'Registering a shared folder...', 'process-shared-folder-imap-ready' => 'Creating a shared folder...', 'process-shared-folder-ldap-ready' => 'Creating a shared folder...', 'distlist-update-success' => 'Distribution list updated successfully.', 'distlist-create-success' => 'Distribution list created successfully.', 'distlist-delete-success' => 'Distribution list deleted successfully.', 'distlist-suspend-success' => 'Distribution list suspended successfully.', 'distlist-unsuspend-success' => 'Distribution list unsuspended successfully.', 'distlist-setconfig-success' => 'Distribution list settings updated successfully.', 'domain-create-success' => 'Domain created successfully.', 'domain-delete-success' => 'Domain deleted successfully.', 'domain-notempty-error' => 'Unable to delete a domain with assigned users or other objects.', 'domain-verify-success' => 'Domain verified successfully.', 'domain-verify-error' => 'Domain ownership verification failed.', 'domain-suspend-success' => 'Domain suspended successfully.', 'domain-unsuspend-success' => 'Domain unsuspended successfully.', 'domain-setconfig-success' => 'Domain settings updated successfully.', 'file-create-success' => 'File created successfully.', 'file-delete-success' => 'File deleted successfully.', 'file-update-success' => 'File updated successfully.', 'file-permissions-create-success' => 'File permissions created successfully.', 'file-permissions-update-success' => 'File permissions updated successfully.', 'file-permissions-delete-success' => 'File permissions deleted successfully.', 'resource-update-success' => 'Resource updated successfully.', 'resource-create-success' => 'Resource created successfully.', 'resource-delete-success' => 'Resource deleted successfully.', 'resource-setconfig-success' => 'Resource settings updated successfully.', 'shared-folder-update-success' => 'Shared folder updated successfully.', 'shared-folder-create-success' => 'Shared folder created successfully.', 'shared-folder-delete-success' => 'Shared folder deleted successfully.', 'shared-folder-setconfig-success' => 'Shared folder settings updated successfully.', 'user-update-success' => 'User data updated successfully.', 'user-create-success' => 'User created successfully.', 'user-delete-success' => 'User deleted successfully.', 'user-suspend-success' => 'User suspended successfully.', 'user-unsuspend-success' => 'User unsuspended successfully.', 'user-reset-2fa-success' => '2-Factor authentication reset successfully.', 'user-setconfig-success' => 'User settings updated successfully.', 'user-set-sku-success' => 'The subscription added successfully.', 'user-set-sku-already-exists' => 'The subscription already exists.', 'search-foundxdomains' => ':x domains have been found.', 'search-foundxdistlists' => ':x distribution lists have been found.', 'search-foundxresources' => ':x resources have been found.', - 'search-foundxsharedfolders' => ':x shared folders have been found.', + 'search-foundxshared-folders' => ':x shared folders have been found.', 'search-foundxusers' => ':x user accounts have been found.', 'signup-invitations-created' => 'The invitation has been created.|:count invitations has been created.', 'signup-invitations-csv-empty' => 'Failed to find any valid email addresses in the uploaded file.', 'signup-invitations-csv-invalid-email' => 'Found an invalid email address (:email) on line :line.', 'signup-invitation-delete-success' => 'Invitation deleted successfully.', 'signup-invitation-resend-success' => 'Invitation added to the sending queue successfully.', 'support-request-success' => 'Support request submitted successfully.', 'support-request-error' => 'Failed to submit the support request.', 'siteuser' => ':site User', 'wallet-award-success' => 'The bonus has been added to the wallet successfully.', 'wallet-penalty-success' => 'The penalty has been added to the wallet successfully.', 'wallet-update-success' => 'User wallet updated successfully.', 'password-reset-code-delete-success' => 'Password reset code deleted successfully.', 'password-rule-min' => 'Minimum password length: :param characters', 'password-rule-max' => 'Maximum password length: :param characters', 'password-rule-lower' => 'Password contains a lower-case character', 'password-rule-upper' => 'Password contains an upper-case character', 'password-rule-digit' => 'Password contains a digit', 'password-rule-special' => 'Password contains a special character', 'password-rule-last' => 'Password cannot be the same as the last :param passwords', 'wallet-notice-date' => 'With your current subscriptions your account balance will last until about :date (:days).', 'wallet-notice-nocredit' => 'You are out of credit, top up your balance now.', 'wallet-notice-today' => 'You will run out of credit today, top up your balance now.', 'wallet-notice-trial' => 'You are in your free trial period.', 'wallet-notice-trial-end' => 'Your free trial is about to end, top up to continue.', ]; diff --git a/src/resources/lang/fr/app.php b/src/resources/lang/fr/app.php index af88b013..6c0f65cb 100644 --- a/src/resources/lang/fr/app.php +++ b/src/resources/lang/fr/app.php @@ -1,114 +1,115 @@ "Crée", 'chart-deleted' => "Supprimé", 'chart-average' => "moyenne", 'chart-allusers' => "Tous Utilisateurs - l'année derniere", 'chart-discounts' => "Rabais", 'chart-vouchers' => "Coupons", 'chart-income' => "Revenus en :currency - 8 dernières semaines", 'chart-users' => "Utilisateurs - 8 dernières semaines", 'mandate-delete-success' => "L'auto-paiement a été supprimé.", 'mandate-update-success' => "L'auto-paiement a été mis-à-jour.", 'planbutton' => "Choisir :plan", 'siteuser' => "Utilisateur du :site", 'domain-setconfig-success' => "Les paramètres du domaine sont mis à jour avec succès.", 'user-setconfig-success' => "Les paramètres d'utilisateur sont mis à jour avec succès.", 'process-async' => "Le processus d'installation a été poussé. Veuillez patienter.", 'process-user-new' => "Enregistrement d'un utilisateur...", 'process-user-ldap-ready' => "Création d'un utilisateur...", 'process-user-imap-ready' => "Création d'une boîte aux lettres...", 'process-distlist-new' => "Enregistrement d'une liste de distribution...", 'process-distlist-ldap-ready' => "Création d'une liste de distribution...", 'process-domain-new' => "Enregistrement d'un domaine personnalisé...", 'process-domain-ldap-ready' => "Création d'un domaine personnalisé...", 'process-domain-verified' => "Vérification d'un domaine personnalisé...", 'process-domain-confirmed' => "vérification de la propriété d'un domaine personnalisé...", 'process-success' => "Le processus d'installation s'est terminé avec succès.", 'process-error-domain-ldap-ready' => "Échec de créer un domaine.", 'process-error-domain-verified' => "Échec de vérifier un domaine.", 'process-error-domain-confirmed' => "Échec de la vérification de la propriété d'un domaine.", 'process-error-distlist-ldap-ready' => "Échec de créer une liste de distrubion.", 'process-error-resource-imap-ready' => "Échec de la vérification de l'existence d'un dossier partagé.", 'process-error-resource-ldap-ready' => "Échec de la création d'une ressource.", 'process-error-shared-folder-imap-ready' => "Impossible de vérifier qu'un dossier partagé existe.", 'process-error-shared-folder-ldap-ready' => "Échec de la création d'un dossier partagé.", 'process-error-user-ldap-ready' => "Échec de la création d'un utilisateur.", 'process-error-user-imap-ready' => "Échec de la vérification de l'existence d'une boîte aux lettres.", 'process-resource-new' => "Enregistrement d'une ressource...", 'process-resource-imap-ready' => "Création d'un dossier partagé...", 'process-resource-ldap-ready' => "Création d'un ressource...", 'process-shared-folder-new' => "Enregistrement d'un dossier partagé...", 'process-shared-folder-imap-ready' => "Création d'un dossier partagé...", 'process-shared-folder-ldap-ready' => "Création d'un dossier partagé...", 'distlist-update-success' => "Liste de distribution mis-à-jour avec succès.", 'distlist-create-success' => "Liste de distribution créer avec succès.", 'distlist-delete-success' => "Liste de distribution suppriméee avec succès.", 'distlist-suspend-success' => "Liste de distribution à été suspendue avec succès.", 'distlist-unsuspend-success' => "Liste de distribution à été débloquée avec succès.", 'distlist-setconfig-success' => "Mise à jour des paramètres de la liste de distribution avec succès.", 'domain-create-success' => "Domaine a été crée avec succès.", 'domain-delete-success' => "Domaine supprimé avec succès.", 'domain-verify-success' => "Domaine vérifié avec succès.", 'domain-verify-error' => "Vérification de propriété de domaine à échoué.", 'domain-suspend-success' => "Domaine suspendue avec succès.", 'domain-unsuspend-success' => "Domaine debloqué avec succès.", 'resource-update-success' => "Ressource mise à jour avec succès.", 'resource-create-success' => "Resource crée avec succès.", 'resource-delete-success' => "Ressource suprimmée avec succès.", 'resource-setconfig-success' => "Les paramètres des ressources ont été mis à jour avec succès.", 'shared-folder-update-success' => "Dossier partagé mis à jour avec succès.", 'shared-folder-create-success' => "Dossier partagé créé avec succès.", 'shared-folder-delete-success' => "Dossier partagé supprimé avec succès.", 'shared-folder-setconfig-success' => "Mise à jour des paramètres du dossier partagé avec succès.", 'user-update-success' => "Mis-à-jour des données de l'utilsateur effectué avec succès.", 'user-create-success' => "Utilisateur a été crée avec succès.", 'user-delete-success' => "Utilisateur a été supprimé avec succès.", 'user-suspend-success' => "Utilisateur a été suspendu avec succès.", 'user-unsuspend-success' => "Utilisateur a été debloqué avec succès.", 'user-reset-2fa-success' => "Réinstallation de l'authentification à 2-Facteur avec succès.", 'user-set-sku-success' => "Souscription ajoutée avec succès.", 'user-set-sku-already-exists' => "La souscription existe déjà.", 'search-foundxdomains' => "Les domaines :x ont été trouvés.", 'search-foundxdistlists' => "Les listes de distribution :x ont été trouvés.", + 'search-foundxresources' => "Les ressources :x ont été trouvés.", 'search-foundxusers' => "Les comptes d'utilisateurs :x ont été trouvés.", - 'search-foundxsharedfolders' => ":x dossiers partagés ont été trouvés.", + 'search-foundxshared-folders' => ":x dossiers partagés ont été trouvés.", 'signup-invitations-created' => "L'invitation à été crée.|:count nombre d'invitations ont été crée.", 'signup-invitations-csv-empty' => "Aucune adresses email valides ont été trouvées dans le fichier téléchargé.", 'signup-invitations-csv-invalid-email' => "Une adresse email invalide a été trouvée (:email) on line :line.", 'signup-invitation-delete-success' => "Invitation supprimée avec succès.", 'signup-invitation-resend-success' => "Invitation ajoutée à la file d'attente d'envoi avec succès.", 'support-request-success' => "Demande de soutien soumise avec succès.", 'support-request-error' => "La soumission de demande de soutien a échoué.", 'wallet-award-success' => "Le bonus a été ajouté au portefeuille avec succès.", 'wallet-penalty-success' => "La pénalité a été ajoutée au portefeuille avec succès.", 'wallet-update-success' => "Portefeuille d'utilisateur a été mis-à-jour avec succès.", 'wallet-notice-date' => "Avec vos abonnements actuels, le solde de votre compte durera jusqu'à environ :date (:days).", 'wallet-notice-nocredit' => "Votre crédit a été epuisé, veuillez recharger immédiatement votre solde.", 'wallet-notice-today' => "Votre reste crédit sera épuisé aujourd'hui, veuillez recharger immédiatement.", 'wallet-notice-trial' => "Vous êtes dans votre période d'essai gratuite.", 'wallet-notice-trial-end' => "Vous approchez de la fin de votre période d'essai gratuite, veuillez recharger pour continuer.", ]; diff --git a/src/resources/vue/Distlist/List.vue b/src/resources/vue/Distlist/List.vue index 8ed1e34b..ccc39181 100644 --- a/src/resources/vue/Distlist/List.vue +++ b/src/resources/vue/Distlist/List.vue @@ -1,64 +1,64 @@ diff --git a/src/resources/vue/Domain/List.vue b/src/resources/vue/Domain/List.vue index 94f48c98..a4813e9c 100644 --- a/src/resources/vue/Domain/List.vue +++ b/src/resources/vue/Domain/List.vue @@ -1,59 +1,59 @@ diff --git a/src/resources/vue/Resource/Info.vue b/src/resources/vue/Resource/Info.vue index 9e2caddd..1858d648 100644 --- a/src/resources/vue/Resource/Info.vue +++ b/src/resources/vue/Resource/Info.vue @@ -1,182 +1,182 @@ diff --git a/src/resources/vue/Resource/List.vue b/src/resources/vue/Resource/List.vue index 6dc7d7ff..e8c7f196 100644 --- a/src/resources/vue/Resource/List.vue +++ b/src/resources/vue/Resource/List.vue @@ -1,64 +1,64 @@ diff --git a/src/resources/vue/SharedFolder/Info.vue b/src/resources/vue/SharedFolder/Info.vue index 1e38a2ff..69c33895 100644 --- a/src/resources/vue/SharedFolder/Info.vue +++ b/src/resources/vue/SharedFolder/Info.vue @@ -1,176 +1,176 @@ diff --git a/src/resources/vue/SharedFolder/List.vue b/src/resources/vue/SharedFolder/List.vue index e88bf31d..917730b9 100644 --- a/src/resources/vue/SharedFolder/List.vue +++ b/src/resources/vue/SharedFolder/List.vue @@ -1,63 +1,63 @@ diff --git a/src/tests/Feature/Controller/DomainsTest.php b/src/tests/Feature/Controller/DomainsTest.php index 82a9e824..98180189 100644 --- a/src/tests/Feature/Controller/DomainsTest.php +++ b/src/tests/Feature/Controller/DomainsTest.php @@ -1,555 +1,564 @@ deleteTestUser('test1@' . \config('app.domain')); $this->deleteTestUser('test2@' . \config('app.domain')); $this->deleteTestUser('test1@domainscontroller.com'); $this->deleteTestDomain('domainscontroller.com'); } 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'); $domain = $this->getTestDomain('kolab.org'); $domain->settings()->whereIn('key', ['spf_whitelist'])->delete(); parent::tearDown(); } /** * Test domain confirm request */ public function testConfirm(): void { Queue::fake(); $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $user = $this->getTestUser('test1@domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('error', $json['status']); $this->assertEquals('Domain ownership verification failed.', $json['message']); $domain->status |= Domain::STATUS_CONFIRMED; $domain->save(); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain verified successfully.', $json['message']); $this->assertTrue(is_array($json['statusInfo'])); // Not authorized access $response = $this->actingAs($john)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(403); // Authorized access by additional account controller $domain = $this->getTestDomain('kolab.org'); $response = $this->actingAs($ned)->get("api/v4/domains/{$domain->id}/confirm"); $response->assertStatus(200); } /** * Test domain delete request (DELETE /api/v4/domains/) */ public function testDestroy(): void { Queue::fake(); $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $john = $this->getTestUser('john@kolab.org'); $johns_domain = $this->getTestDomain('kolab.org'); $user1 = $this->getTestUser('test1@' . \config('app.domain')); $user2 = $this->getTestUser('test2@' . \config('app.domain')); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user1->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); // Not authorized access $response = $this->actingAs($john)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(403); // Can't delete non-empty domain $response = $this->actingAs($john)->delete("api/v4/domains/{$johns_domain->id}"); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('error', $json['status']); $this->assertEquals('Unable to delete a domain with assigned users or other objects.', $json['message']); // Successful deletion $response = $this->actingAs($user1)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain deleted successfully.', $json['message']); $this->assertTrue($domain->fresh()->trashed()); // Authorized access by additional account controller $this->deleteTestDomain('domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); Entitlement::create([ 'wallet_id' => $user1->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $user1->wallets()->first()->addController($user2); $response = $this->actingAs($user2)->delete("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertEquals('success', $json['status']); $this->assertEquals('Domain deleted successfully.', $json['message']); $this->assertTrue($domain->fresh()->trashed()); } /** * Test fetching domains list */ public function testIndex(): void { // User with no domains $user = $this->getTestUser('test1@domainscontroller.com'); $response = $this->actingAs($user)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); - $this->assertSame([], $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(1, $json); - $this->assertSame('kolab.org', $json[0]['namespace']); + $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[0]); - $this->assertArrayHasKey('isDeleted', $json[0]); - $this->assertArrayHasKey('isVerified', $json[0]); - $this->assertArrayHasKey('isSuspended', $json[0]); - $this->assertArrayHasKey('isActive', $json[0]); - $this->assertArrayHasKey('isLdapReady', $json[0]); + $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]); + $this->assertArrayHasKey('isLdapReady', $json['list'][0]); $response = $this->actingAs($ned)->get("api/v4/domains"); $response->assertStatus(200); $json = $response->json(); - $this->assertCount(1, $json); - $this->assertSame('kolab.org', $json[0]['namespace']); + $this->assertCount(4, $json); + $this->assertCount(1, $json['list']); + $this->assertSame('kolab.org', $json['list'][0]['namespace']); } /** * Test domain config update (POST /api/v4/domains//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $domain = $this->getTestDomain('kolab.org'); $domain->setSetting('spf_whitelist', null); // Test unknown domain id $post = ['spf_whitelist' => []]; $response = $this->actingAs($john)->post("/api/v4/domains/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['spf_whitelist' => []]; $response = $this->actingAs($jack)->post("/api/v4/domains/{$domain->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['grey' => 1]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['grey']); $this->assertNull($domain->fresh()->getSetting('spf_whitelist')); // Test some valid data $post = ['spf_whitelist' => ['.test.domain.com']]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame('Domain settings updated successfully.', $json['message']); $expected = \json_encode($post['spf_whitelist']); $this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist')); // Test input validation $post = ['spf_whitelist' => ['aaa']]; $response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame( 'The entry format is invalid. Expected a domain name starting with a dot.', $json['errors']['spf_whitelist'][0] ); $this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist')); } /** * Test fetching domain info */ public function testShow(): void { $sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first(); $user = $this->getTestUser('test1@domainscontroller.com'); $domain = $this->getTestDomain('domainscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_EXTERNAL, ]); $discount = \App\Discount::withEnvTenantContext()->where('code', 'TEST')->first(); $wallet = $user->wallet(); $wallet->discount()->associate($discount); $wallet->save(); Entitlement::create([ 'wallet_id' => $user->wallets()->first()->id, 'sku_id' => $sku_domain->id, 'entitleable_id' => $domain->id, 'entitleable_type' => Domain::class ]); $response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals($domain->id, $json['id']); $this->assertEquals($domain->namespace, $json['namespace']); $this->assertEquals($domain->status, $json['status']); $this->assertEquals($domain->type, $json['type']); $this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']); $this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']); $this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']); $this->assertSame([], $json['config']['spf_whitelist']); $this->assertCount(4, $json['mx']); $this->assertTrue(strpos(implode("\n", $json['mx']), $domain->namespace) !== false); $this->assertCount(8, $json['dns']); $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false); $this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false); $this->assertTrue(is_array($json['statusInfo'])); // Values below are tested by Unit tests $this->assertArrayHasKey('isConfirmed', $json); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isVerified', $json); $this->assertArrayHasKey('isSuspended', $json); $this->assertArrayHasKey('isActive', $json); $this->assertArrayHasKey('isLdapReady', $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 domain status (GET /api/v4/domains//status) * and forcing setup process update (?refresh=1) * * @group dns */ public function testStatus(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $domain = $this->getTestDomain('kolab.org'); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/domains/{$domain->id}/status"); $response->assertStatus(403); $domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY; $domain->save(); // Get domain status $response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse($json['isVerified']); $this->assertFalse($json['isReady']); $this->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->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 1fa31fc8..65184d50 100644 --- a/src/tests/Feature/Controller/GroupsTest.php +++ b/src/tests/Feature/Controller/GroupsTest.php @@ -1,601 +1,613 @@ deleteTestGroup('group-test@kolab.org'); $this->deleteTestGroup('group-test2@kolab.org'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestGroup('group-test@kolab.org'); $this->deleteTestGroup('group-test2@kolab.org'); parent::tearDown(); } /** * Test group deleting (DELETE /api/v4/groups/) */ public function testDestroy(): void { // First create some groups to delete $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unauth access $response = $this->delete("api/v4/groups/{$group->id}"); $response->assertStatus(401); // Test non-existing group $response = $this->actingAs($john)->delete("api/v4/groups/abc"); $response->assertStatus(404); // Test access to other user's group $response = $this->actingAs($jack)->delete("api/v4/groups/{$group->id}"); $response->assertStatus(403); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test removing a group $response = $this->actingAs($john)->delete("api/v4/groups/{$group->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals("Distribution list deleted successfully.", $json['message']); } /** * Test groups listing (GET /api/v4/groups) */ public function testIndex(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unauth access $response = $this->get("api/v4/groups"); $response->assertStatus(401); // Test a user with no groups $response = $this->actingAs($jack)->get("/api/v4/groups"); $response->assertStatus(200); $json = $response->json(); - $this->assertCount(0, $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(1, $json); - $this->assertSame($group->id, $json[0]['id']); - $this->assertSame($group->email, $json[0]['email']); - $this->assertSame($group->name, $json[0]['name']); - $this->assertArrayHasKey('isDeleted', $json[0]); - $this->assertArrayHasKey('isSuspended', $json[0]); - $this->assertArrayHasKey('isActive', $json[0]); - $this->assertArrayHasKey('isLdapReady', $json[0]); + $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(1, $json); - $this->assertSame($group->email, $json[0]['email']); + $this->assertCount(4, $json); + $this->assertSame(1, $json['count']); + $this->assertSame(false, $json['hasMore']); + $this->assertSame("1 distribution lists have been found.", $json['message']); + $this->assertCount(1, $json['list']); + $this->assertSame($group->email, $json['list'][0]['email']); } /** * Test group config update (POST /api/v4/groups//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unknown group id $post = ['sender_policy' => []]; $response = $this->actingAs($john)->post("/api/v4/groups/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['sender_policy' => []]; $response = $this->actingAs($jack)->post("/api/v4/groups/{$group->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['test' => 1]; $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']); $group->refresh(); $this->assertNull($group->getSetting('test')); $this->assertNull($group->getSetting('sender_policy')); // Test some valid data $post = ['sender_policy' => ['domain.com']]; $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame('Distribution list settings updated successfully.', $json['message']); $this->assertSame(['sender_policy' => $post['sender_policy']], $group->fresh()->getConfig()); // Test input validation $post = ['sender_policy' => [5]]; $response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame( 'The entry format is invalid. Expected an email, domain, or part of it.', $json['errors']['sender_policy'][0] ); $this->assertSame(['sender_policy' => ['domain.com']], $group->fresh()->getConfig()); } /** * Test fetching group data/profile (GET /api/v4/groups/) */ public function testShow(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); $group->setSetting('sender_policy', '["test"]'); // Test unauthorized access to a profile of other user $response = $this->get("/api/v4/groups/{$group->id}"); $response->assertStatus(401); // Test unauthorized access to a group of another user $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}"); $response->assertStatus(403); // John: Group owner - non-existing group $response = $this->actingAs($john)->get("/api/v4/groups/abc"); $response->assertStatus(404); // John: Group owner $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($group->id, $json['id']); $this->assertSame($group->email, $json['email']); $this->assertSame($group->name, $json['name']); $this->assertSame($group->members, $json['members']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isSuspended', $json); $this->assertArrayHasKey('isActive', $json); $this->assertArrayHasKey('isLdapReady', $json); $this->assertSame(['sender_policy' => ['test']], $json['config']); } /** * Test fetching group status (GET /api/v4/groups//status) * and forcing setup process update (?refresh=1) */ public function testStatus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unauthorized access $response = $this->get("/api/v4/groups/abc/status"); $response->assertStatus(401); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}/status"); $response->assertStatus(403); $group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE; $group->save(); // Get group status $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse($json['isLdapReady']); $this->assertFalse($json['isReady']); $this->assertFalse($json['isSuspended']); $this->assertTrue($json['isActive']); $this->assertFalse($json['isDeleted']); $this->assertCount(6, $json['process']); $this->assertSame('distlist-new', $json['process'][0]['label']); $this->assertSame(true, $json['process'][0]['state']); $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']); $this->assertSame(false, $json['process'][1]['state']); $this->assertTrue(empty($json['status'])); $this->assertTrue(empty($json['message'])); // Make sure the domain is confirmed (other test might unset that status) $domain = $this->getTestDomain('kolab.org'); $domain->status |= \App\Domain::STATUS_CONFIRMED; $domain->save(); // Now "reboot" the process and the group $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isLdapReady']); $this->assertTrue($json['isReady']); $this->assertCount(6, $json['process']); $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']); $this->assertSame(true, $json['process'][1]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); // Test a case when a domain is not ready $domain->status ^= \App\Domain::STATUS_CONFIRMED; $domain->save(); $response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isLdapReady']); $this->assertTrue($json['isReady']); $this->assertCount(6, $json['process']); $this->assertSame('distlist-ldap-ready', $json['process'][1]['label']); $this->assertSame(true, $json['process'][1]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); } /** * Test GroupsController::statusInfo() */ public function testStatusInfo(): void { $john = $this->getTestUser('john@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); $group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE; $group->save(); $result = GroupsController::statusInfo($group); $this->assertFalse($result['isReady']); $this->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->assertCount(6, $result['process']); $this->assertSame('distlist-new', $result['process'][0]['label']); $this->assertSame(true, $result['process'][0]['state']); $this->assertSame('distlist-ldap-ready', $result['process'][1]['label']); $this->assertSame(true, $result['process'][2]['state']); $this->assertSame('done', $result['processState']); } /** * Test group creation (POST /api/v4/groups) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test unauth request $response = $this->post("/api/v4/groups", []); $response->assertStatus(401); // Test non-controller user $response = $this->actingAs($jack)->post("/api/v4/groups", []); $response->assertStatus(403); // Test empty request $response = $this->actingAs($john)->post("/api/v4/groups", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The email field is required.", $json['errors']['email']); $this->assertSame("At least one recipient is required.", $json['errors']['members']); $this->assertSame("The name field is required.", $json['errors']['name'][0]); $this->assertCount(2, $json); $this->assertCount(3, $json['errors']); // Test missing members and name $post = ['email' => 'group-test@kolab.org']; $response = $this->actingAs($john)->post("/api/v4/groups", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertSame("At least one recipient is required.", $json['errors']['members']); $this->assertSame("The name field is required.", $json['errors']['name'][0]); $this->assertCount(2, $json); $this->assertCount(2, $json['errors']); // Test invalid email and too long name $post = ['email' => 'invalid', 'name' => str_repeat('A', 192)]; $response = $this->actingAs($john)->post("/api/v4/groups", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame("The specified email is invalid.", $json['errors']['email']); $this->assertSame("The name may not be greater than 191 characters.", $json['errors']['name'][0]); $this->assertCount(3, $json['errors']); // Test successful group creation $post = [ 'name' => 'Test Group', 'email' => 'group-test@kolab.org', 'members' => ['test1@domain.tld', 'test2@domain.tld'] ]; $response = $this->actingAs($john)->post("/api/v4/groups", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Distribution list created successfully.", $json['message']); $this->assertCount(2, $json); $group = Group::where('email', 'group-test@kolab.org')->first(); $this->assertInstanceOf(Group::class, $group); $this->assertSame($post['email'], $group->email); $this->assertSame($post['members'], $group->members); $this->assertTrue($john->groups()->get()->contains($group)); // Group name must be unique within a domain $post['email'] = 'group-test2@kolab.org'; $post['members'] = ['test1@domain.tld']; $response = $this->actingAs($john)->post("/api/v4/groups", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame("The specified name is not available.", $json['errors']['name'][0]); } /** * Test group update (PUT /api/v4/groups/) */ public function testUpdate(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); $group->assignToWallet($john->wallets->first()); // Test unauthorized update $response = $this->get("/api/v4/groups/{$group->id}", []); $response->assertStatus(401); // Test unauthorized update $response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}", []); $response->assertStatus(403); // Test updating - missing members $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("At least one recipient is required.", $json['errors']['members']); $this->assertCount(2, $json); // Test some invalid data $post = ['members' => ['test@domain.tld', 'invalid']]; $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame('The specified email address is invalid.', $json['errors']['members'][1]); // Valid data - members and name changed $post = [ 'name' => 'Test Gr', 'members' => ['member1@test.domain', 'member2@test.domain'] ]; $response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Distribution list updated successfully.", $json['message']); $this->assertCount(2, $json); $group->refresh(); $this->assertSame($post['name'], $group->name); $this->assertSame($post['members'], $group->members); } /** * Group email address validation. */ public function testValidateGroupEmail(): void { $john = $this->getTestUser('john@kolab.org'); $group = $this->getTestGroup('group-test@kolab.org'); // Invalid email $result = GroupsController::validateGroupEmail('', $john); $this->assertSame("The email field is required.", $result); $result = GroupsController::validateGroupEmail('kolab.org', $john); $this->assertSame("The specified email is invalid.", $result); $result = GroupsController::validateGroupEmail('.@kolab.org', $john); $this->assertSame("The specified email is invalid.", $result); $result = GroupsController::validateGroupEmail('test123456@localhost', $john); $this->assertSame("The specified domain is invalid.", $result); $result = GroupsController::validateGroupEmail('test123456@unknown-domain.org', $john); $this->assertSame("The specified domain is invalid.", $result); // forbidden public domain $result = GroupsController::validateGroupEmail('testtest@kolabnow.com', $john); $this->assertSame("The specified domain is not available.", $result); // existing alias $result = GroupsController::validateGroupEmail('jack.daniels@kolab.org', $john); $this->assertSame("The specified email is not available.", $result); // existing user $result = GroupsController::validateGroupEmail('ned@kolab.org', $john); $this->assertSame("The specified email is not available.", $result); // existing group $result = GroupsController::validateGroupEmail('group-test@kolab.org', $john); $this->assertSame("The specified email is not available.", $result); // valid $result = GroupsController::validateGroupEmail('admin@kolab.org', $john); $this->assertSame(null, $result); } /** * Group member email address validation. */ public function testValidateMemberEmail(): void { $john = $this->getTestUser('john@kolab.org'); // Invalid format $result = GroupsController::validateMemberEmail('kolab.org', $john); $this->assertSame("The specified email address is invalid.", $result); $result = GroupsController::validateMemberEmail('.@kolab.org', $john); $this->assertSame("The specified email address is invalid.", $result); $result = GroupsController::validateMemberEmail('test123456@localhost', $john); $this->assertSame("The specified email address is invalid.", $result); // Test local non-existing user $result = GroupsController::validateMemberEmail('unknown@kolab.org', $john); $this->assertSame("The specified email address does not exist.", $result); // Test local existing user $result = GroupsController::validateMemberEmail('ned@kolab.org', $john); $this->assertSame(null, $result); // Test existing user, but not in the same account $result = GroupsController::validateMemberEmail('jeroen@jeroen.jeroen', $john); $this->assertSame(null, $result); // Valid address $result = GroupsController::validateMemberEmail('test@google.com', $john); $this->assertSame(null, $result); } } diff --git a/src/tests/Feature/Controller/ResourcesTest.php b/src/tests/Feature/Controller/ResourcesTest.php index da6c8aa7..7a5ccc8d 100644 --- a/src/tests/Feature/Controller/ResourcesTest.php +++ b/src/tests/Feature/Controller/ResourcesTest.php @@ -1,482 +1,494 @@ deleteTestResource('resource-test@kolab.org'); Resource::where('name', 'Test Resource')->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestResource('resource-test@kolab.org'); Resource::where('name', 'Test Resource')->delete(); parent::tearDown(); } /** * Test resource deleting (DELETE /api/v4/resources/) */ public function testDestroy(): void { // First create some groups to delete $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Test unauth access $response = $this->delete("api/v4/resources/{$resource->id}"); $response->assertStatus(401); // Test non-existing resource $response = $this->actingAs($john)->delete("api/v4/resources/abc"); $response->assertStatus(404); // Test access to other user's resource $response = $this->actingAs($jack)->delete("api/v4/resources/{$resource->id}"); $response->assertStatus(403); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test removing a resource $response = $this->actingAs($john)->delete("api/v4/resources/{$resource->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals("Resource deleted successfully.", $json['message']); } /** * Test resources listing (GET /api/v4/resources) */ public function testIndex(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); // Test unauth access $response = $this->get("api/v4/resources"); $response->assertStatus(401); // Test a user with no resources $response = $this->actingAs($jack)->get("/api/v4/resources"); $response->assertStatus(200); $json = $response->json(); - $this->assertCount(0, $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(2, $json); - $this->assertSame($resource->id, $json[0]['id']); - $this->assertSame($resource->email, $json[0]['email']); - $this->assertSame($resource->name, $json[0]['name']); - $this->assertArrayHasKey('isDeleted', $json[0]); - $this->assertArrayHasKey('isActive', $json[0]); - $this->assertArrayHasKey('isLdapReady', $json[0]); - $this->assertArrayHasKey('isImapReady', $json[0]); + $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]); + $this->assertArrayHasKey('isImapReady', $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(2, $json); - $this->assertSame($resource->email, $json[0]['email']); + $this->assertCount(4, $json); + $this->assertSame(2, $json['count']); + $this->assertSame(false, $json['hasMore']); + $this->assertSame("2 resources have been found.", $json['message']); + $this->assertCount(2, $json['list']); + $this->assertSame($resource->email, $json['list'][0]['email']); } /** * Test resource config update (POST /api/v4/resources//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Test unknown resource id $post = ['invitation_policy' => 'reject']; $response = $this->actingAs($john)->post("/api/v4/resources/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $post = ['invitation_policy' => 'reject']; $response = $this->actingAs($jack)->post("/api/v4/resources/{$resource->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['test' => 1]; $response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']); $resource->refresh(); $this->assertNull($resource->getSetting('test')); $this->assertNull($resource->getSetting('invitation_policy')); // Test some valid data $post = ['invitation_policy' => 'reject']; $response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame("Resource settings updated successfully.", $json['message']); $this->assertSame(['invitation_policy' => 'reject'], $resource->fresh()->getConfig()); // Test input validation $post = ['invitation_policy' => 'aaa']; $response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertSame( "The specified invitation policy is invalid.", $json['errors']['invitation_policy'] ); $this->assertSame(['invitation_policy' => 'reject'], $resource->fresh()->getConfig()); } /** * Test fetching resource data/profile (GET /api/v4/resources/) */ public function testShow(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); $resource->setSetting('invitation_policy', 'reject'); // Test unauthorized access to a profile of other user $response = $this->get("/api/v4/resources/{$resource->id}"); $response->assertStatus(401); // Test unauthorized access to a resource of another user $response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}"); $response->assertStatus(403); // John: Account owner - non-existing resource $response = $this->actingAs($john)->get("/api/v4/resources/abc"); $response->assertStatus(404); // John: Account owner $response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($resource->id, $json['id']); $this->assertSame($resource->email, $json['email']); $this->assertSame($resource->name, $json['name']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isActive', $json); $this->assertArrayHasKey('isLdapReady', $json); $this->assertArrayHasKey('isImapReady', $json); $this->assertSame(['invitation_policy' => 'reject'], $json['config']); } /** * Test fetching a resource status (GET /api/v4/resources//status) * and forcing setup process update (?refresh=1) */ public function testStatus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Test unauthorized access $response = $this->get("/api/v4/resources/abc/status"); $response->assertStatus(401); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}/status"); $response->assertStatus(403); $resource->status = Resource::STATUS_NEW | Resource::STATUS_ACTIVE; $resource->save(); // Get resource status $response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}/status"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse($json['isLdapReady']); $this->assertFalse($json['isImapReady']); $this->assertFalse($json['isReady']); $this->assertFalse($json['isDeleted']); $this->assertTrue($json['isActive']); $this->assertCount(7, $json['process']); $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']); $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 and get the resource status $response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isLdapReady']); $this->assertTrue($json['isImapReady']); $this->assertTrue($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('resource-ldap-ready', $json['process'][1]['label']); $this->assertSame(true, $json['process'][1]['state']); $this->assertSame('resource-imap-ready', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); $this->assertSame('done', $json['processState']); // Test a case when a domain is not ready $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->assertTrue($json['isLdapReady']); $this->assertTrue($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('resource-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 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']); $this->assertCount(7, $result['process']); $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']); $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']); $this->assertCount(7, $result['process']); $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']); $this->assertSame('done', $result['processState']); } /** * Test resource creation (POST /api/v4/resources) */ public function testStore(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); // Test unauth request $response = $this->post("/api/v4/resources", []); $response->assertStatus(401); // Test non-controller user $response = $this->actingAs($jack)->post("/api/v4/resources", []); $response->assertStatus(403); // Test empty request $response = $this->actingAs($john)->post("/api/v4/resources", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("The name field is required.", $json['errors']['name'][0]); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); // Test too long name $post = ['domain' => 'kolab.org', 'name' => str_repeat('A', 192)]; $response = $this->actingAs($john)->post("/api/v4/resources", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertSame("The name may not be greater than 191 characters.", $json['errors']['name'][0]); $this->assertCount(1, $json['errors']); // Test successful resource creation $post['name'] = 'Test Resource'; $response = $this->actingAs($john)->post("/api/v4/resources", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Resource created successfully.", $json['message']); $this->assertCount(2, $json); $resource = Resource::where('name', $post['name'])->first(); $this->assertInstanceOf(Resource::class, $resource); $this->assertTrue($john->resources()->get()->contains($resource)); // Resource name must be unique within a domain $response = $this->actingAs($john)->post("/api/v4/resources", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame("The specified name is not available.", $json['errors']['name'][0]); } /** * Test resource update (PUT /api/v4/resources/) */ public function testUpdate(): void { Queue::fake(); $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $resource = $this->getTestResource('resource-test@kolab.org'); $resource->assignToWallet($john->wallets->first()); // Test unauthorized update $response = $this->get("/api/v4/resources/{$resource->id}", []); $response->assertStatus(401); // Test unauthorized update $response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}", []); $response->assertStatus(403); // Name change $post = [ 'name' => 'Test Res', ]; $response = $this->actingAs($john)->put("/api/v4/resources/{$resource->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Resource updated successfully.", $json['message']); $this->assertCount(2, $json); $resource->refresh(); $this->assertSame($post['name'], $resource->name); } } diff --git a/src/tests/Feature/Controller/SharedFoldersTest.php b/src/tests/Feature/Controller/SharedFoldersTest.php index 9518f63b..f469d054 100644 --- a/src/tests/Feature/Controller/SharedFoldersTest.php +++ b/src/tests/Feature/Controller/SharedFoldersTest.php @@ -1,550 +1,562 @@ deleteTestSharedFolder('folder-test@kolab.org'); SharedFolder::where('name', 'Test Folder')->delete(); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestSharedFolder('folder-test@kolab.org'); SharedFolder::where('name', 'Test Folder')->delete(); parent::tearDown(); } /** * Test resource deleting (DELETE /api/v4/resources/) */ public function testDestroy(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Test unauth access $response = $this->delete("api/v4/shared-folders/{$folder->id}"); $response->assertStatus(401); // Test non-existing folder $response = $this->actingAs($john)->delete("api/v4/shared-folders/abc"); $response->assertStatus(404); // Test access to other user's folder $response = $this->actingAs($jack)->delete("api/v4/shared-folders/{$folder->id}"); $response->assertStatus(403); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test removing a folder $response = $this->actingAs($john)->delete("api/v4/shared-folders/{$folder->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals("Shared folder deleted successfully.", $json['message']); } /** * Test shared folders listing (GET /api/v4/shared-folders) */ public function testIndex(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); // Test unauth access $response = $this->get("api/v4/shared-folders"); $response->assertStatus(401); // Test a user with no shared folders $response = $this->actingAs($jack)->get("/api/v4/shared-folders"); $response->assertStatus(200); $json = $response->json(); - $this->assertCount(0, $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(2, $json); - $this->assertSame($folder->id, $json[0]['id']); - $this->assertSame($folder->email, $json[0]['email']); - $this->assertSame($folder->name, $json[0]['name']); - $this->assertSame($folder->type, $json[0]['type']); - $this->assertArrayHasKey('isDeleted', $json[0]); - $this->assertArrayHasKey('isActive', $json[0]); - $this->assertArrayHasKey('isLdapReady', $json[0]); - $this->assertArrayHasKey('isImapReady', $json[0]); + $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]); + $this->assertArrayHasKey('isImapReady', $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(2, $json); - $this->assertSame($folder->email, $json[0]['email']); + $this->assertCount(4, $json); + $this->assertSame(2, $json['count']); + $this->assertSame(false, $json['hasMore']); + $this->assertSame("2 shared folders have been found.", $json['message']); + $this->assertCount(2, $json['list']); + $this->assertSame($folder->email, $json['list'][0]['email']); } /** * Test shared folder config update (POST /api/v4/shared-folders//config) */ public function testSetConfig(): void { $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Test unknown resource id $post = ['acl' => ['john@kolab.org, full']]; $response = $this->actingAs($john)->post("/api/v4/shared-folders/123/config", $post); $json = $response->json(); $response->assertStatus(404); // Test access by user not being a wallet controller $response = $this->actingAs($jack)->post("/api/v4/shared-folders/{$folder->id}/config", $post); $json = $response->json(); $response->assertStatus(403); $this->assertSame('error', $json['status']); $this->assertSame("Access denied", $json['message']); $this->assertCount(2, $json); // Test some invalid data $post = ['test' => 1]; $response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']); $folder->refresh(); $this->assertNull($folder->getSetting('test')); $this->assertNull($folder->getSetting('acl')); // Test some valid data $post = ['acl' => ['john@kolab.org, full']]; $response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post); $response->assertStatus(200); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('success', $json['status']); $this->assertSame("Shared folder settings updated successfully.", $json['message']); $this->assertSame(['acl' => $post['acl']], $folder->fresh()->getConfig()); // Test input validation $post = ['acl' => ['john@kolab.org, full', 'test, full']]; $response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post); $response->assertStatus(422); $json = $response->json(); $this->assertCount(2, $json); $this->assertSame('error', $json['status']); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['acl']); $this->assertSame( "The specified email address is invalid.", $json['errors']['acl'][1] ); $this->assertSame(['acl' => ['john@kolab.org, full']], $folder->fresh()->getConfig()); } /** * Test fetching shared folder data/profile (GET /api/v4/shared-folders/) */ public function testShow(): void { $jack = $this->getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); $folder->setSetting('acl', '["anyone, full"]'); $folder->setAliases(['folder-alias@kolab.org']); // Test unauthenticated access $response = $this->get("/api/v4/shared-folders/{$folder->id}"); $response->assertStatus(401); // Test unauthorized access to a shared folder of another user $response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}"); $response->assertStatus(403); // John: Account owner - non-existing folder $response = $this->actingAs($john)->get("/api/v4/shared-folders/abc"); $response->assertStatus(404); // John: Account owner $response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}"); $response->assertStatus(200); $json = $response->json(); $this->assertSame($folder->id, $json['id']); $this->assertSame($folder->email, $json['email']); $this->assertSame($folder->name, $json['name']); $this->assertSame($folder->type, $json['type']); $this->assertSame(['folder-alias@kolab.org'], $json['aliases']); $this->assertTrue(!empty($json['statusInfo'])); $this->assertArrayHasKey('isDeleted', $json); $this->assertArrayHasKey('isActive', $json); $this->assertArrayHasKey('isLdapReady', $json); $this->assertArrayHasKey('isImapReady', $json); $this->assertSame(['acl' => ['anyone, full']], $json['config']); } /** * Test fetching a shared folder status (GET /api/v4/shared-folders//status) * and forcing setup process update (?refresh=1) */ public function testStatus(): void { Queue::fake(); $john = $this->getTestUser('john@kolab.org'); $jack = $this->getTestUser('jack@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Test unauthorized access $response = $this->get("/api/v4/shared-folders/abc/status"); $response->assertStatus(401); // Test unauthorized access $response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}/status"); $response->assertStatus(403); $folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE; $folder->save(); // Get resource status $response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}/status"); $response->assertStatus(200); $json = $response->json(); $this->assertFalse($json['isLdapReady']); $this->assertFalse($json['isImapReady']); $this->assertFalse($json['isReady']); $this->assertFalse($json['isDeleted']); $this->assertTrue($json['isActive']); $this->assertCount(7, $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->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(); $folder->status |= SharedFolder::STATUS_IMAP_READY; $folder->save(); // Now "reboot" the process and get the folder status $response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}/status?refresh=1"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue($json['isLdapReady']); $this->assertTrue($json['isImapReady']); $this->assertTrue($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('shared-folder-ldap-ready', $json['process'][1]['label']); $this->assertSame(true, $json['process'][1]['state']); $this->assertSame('shared-folder-imap-ready', $json['process'][2]['label']); $this->assertSame(true, $json['process'][2]['state']); $this->assertSame('success', $json['status']); $this->assertSame('Setup process finished successfully.', $json['message']); $this->assertSame('done', $json['processState']); // Test a case when a domain is not ready $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->assertTrue($json['isLdapReady']); $this->assertTrue($json['isReady']); $this->assertCount(7, $json['process']); $this->assertSame('shared-folder-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 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']); $this->assertCount(7, $result['process']); $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']); $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']); $this->assertCount(7, $result['process']); $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']); } /** * 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'] = ['folder-alias@kolab.org']; // expected to be ignored $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()); } /** * Test shared folder update (PUT /api/v4/shared-folders/getTestUser('jack@kolab.org'); $john = $this->getTestUser('john@kolab.org'); $ned = $this->getTestUser('ned@kolab.org'); $folder = $this->getTestSharedFolder('folder-test@kolab.org'); $folder->assignToWallet($john->wallets->first()); // Test unauthorized update $response = $this->get("/api/v4/shared-folders/{$folder->id}", []); $response->assertStatus(401); // Test unauthorized update $response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}", []); $response->assertStatus(403); // Name change $post = [ 'name' => 'Test Res', ]; $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Shared folder updated successfully.", $json['message']); $this->assertCount(2, $json); $folder->refresh(); $this->assertSame($post['name'], $folder->name); // Aliases with error $post['aliases'] = ['folder-alias1@kolab.org', 'folder-alias2@unknown.com']; $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post); $json = $response->json(); $response->assertStatus(422); $this->assertSame('error', $json['status']); $this->assertCount(2, $json); $this->assertCount(1, $json['errors']); $this->assertCount(1, $json['errors']['aliases']); $this->assertSame("The specified domain is invalid.", $json['errors']['aliases'][1]); $this->assertSame([], $folder->aliases()->pluck('alias')->all()); // Aliases with success expected $post['aliases'] = ['folder-alias1@kolab.org', 'folder-alias2@kolab.org']; $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post); $json = $response->json(); $response->assertStatus(200); $this->assertSame('success', $json['status']); $this->assertSame("Shared folder updated successfully.", $json['message']); $this->assertCount(2, $json); $this->assertSame($post['aliases'], $folder->aliases()->pluck('alias')->all()); // All aliases removal $post['aliases'] = []; $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post); $response->assertStatus(200); $this->assertSame($post['aliases'], $folder->aliases()->pluck('alias')->all()); } }