Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2531489
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
147 KB
Referenced Files
None
Subscribers
None
View Options
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 @@
<?php
namespace App\Http\Controllers\API\V4\Admin;
use App\SharedFolder;
use App\User;
use Illuminate\Http\Request;
class SharedFoldersController extends \App\Http\Controllers\API\V4\SharedFoldersController
{
/**
* Search for shared folders
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$search = trim(request()->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 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Str;
class RelationController extends ResourceController
{
/** @var array Common object properties in the API response */
protected $objectProps = [];
/** @var string Resource localization label */
protected $label = '';
/** @var string Resource model name */
protected $model = '';
/** @var array Resource listing order (column names) */
protected $order = [];
/** @var array Resource relation method arguments */
protected $relationArgs = [];
/**
* Delete a resource.
*
* @param string $id Resource identifier
*
* @return \Illuminate\Http\JsonResponse The response
*/
public function destroy($id)
{
$resource = $this->model::find($id);
if (!$this->checkTenant($resource)) {
return $this->errorResponse(404);
}
if (!$this->guard()->user()->canDelete($resource)) {
return $this->errorResponse(403);
}
$resource->delete();
return response()->json([
'status' => 'success',
'message' => \trans("app.{$this->label}-delete-success"),
]);
}
/**
* Listing of resources belonging to the authenticated user.
*
* The resource entitlements billed to the current user wallet(s)
*
* @return \Illuminate\Http\JsonResponse
*/
public function index()
{
$user = $this->guard()->user();
$method = Str::plural(\lcfirst(\class_basename($this->model)));
$query = call_user_func_array([$user, $method], $this->relationArgs);
if (!empty($this->order)) {
foreach ($this->order as $col) {
$query->orderBy($col);
}
}
+ // TODO: Search and paging
+
$result = $query->get()
->map(function ($resource) {
return $this->objectToClient($resource);
});
+ $result = [
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => false,
+ 'message' => \trans("app.search-foundx{$this->label}s", ['x' => count($result)]),
+ ];
+
return response()->json($result);
}
/**
* Prepare resource statuses for the UI
*
* @param object $resource Resource object
*
* @return array Statuses array
*/
protected static function objectState($resource): array
{
$state = [];
$reflect = new \ReflectionClass(get_class($resource));
foreach (array_keys($reflect->getConstants()) as $const) {
if (strpos($const, 'STATUS_') === 0 && $const != 'STATUS_NEW') {
$method = Str::camel('is_' . strtolower(substr($const, 7)));
$state[$method] = $resource->{$method}();
}
}
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 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used in the application.
*/
'chart-created' => '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 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Application Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used in the application.
*/
'chart-created' => "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 @@
<template>
<div class="container">
<div class="card" id="distlist-list">
<div class="card-body">
<div class="card-title">
{{ $tc('distlist.list-title', 2) }}
<small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
<btn-router v-if="!$root.isDegraded()" class="btn-success float-end" to="distlist/new" icon="users">
{{ $t('distlist.create') }}
</btn-router>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('distlist.name') }}</th>
<th scope="col">{{ $t('distlist.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="list in lists" :key="list.id" @click="$root.clickRecord">
<td>
<svg-icon icon="users" :class="$root.statusClass(list)" :title="$root.statusText(list)"></svg-icon>
<router-link :to="{ path: 'distlist/' + list.id }">{{ list.name }}</router-link>
</td>
<td>
<router-link :to="{ path: 'distlist/' + list.id }">{{ list.email }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('distlist.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
require('@fortawesome/free-solid-svg-icons/faUsers').definition,
)
export default {
data() {
return {
lists: []
}
},
created() {
axios.get('/api/v4/groups', { loader: true })
.then(response => {
- this.lists = response.data
+ this.lists = response.data.list
})
.catch(this.$root.errorHandler)
}
}
</script>
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 @@
<template>
<div class="container">
<div class="card" id="domain-list">
<div class="card-body">
<div class="card-title">
{{ $t('user.domains') }}
<btn-router v-if="!$root.isDegraded()" class="btn-success float-end" to="domain/new" icon="globe">
{{ $t('domain.create') }}
</btn-router>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('domain.namespace') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="domain in domains" :key="domain.id" @click="$root.clickRecord">
<td>
<svg-icon icon="globe" :class="$root.statusClass(domain)" :title="$root.statusText(domain)"></svg-icon>
<router-link :to="{ path: 'domain/' + domain.id }">{{ domain.namespace }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td>{{ $t('user.domains-none') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
require('@fortawesome/free-solid-svg-icons/faGlobe').definition,
)
export default {
data() {
return {
domains: []
}
},
created() {
axios.get('/api/v4/domains', { loader: true })
.then(response => {
- this.domains = response.data
+ this.domains = response.data.list
})
.catch(this.$root.errorHandler)
}
}
</script>
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 @@
<template>
<div class="container">
<status-component v-if="resource_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
<div class="card" id="resource-info">
<div class="card-body">
<div class="card-title" v-if="resource_id !== 'new'">
{{ $tc('resource.list-title', 1) }}
<btn class="btn-outline-danger button-delete float-end" @click="deleteResource()" icon="trash-can">{{ $t('resource.delete') }}</btn>
</div>
<div class="card-title" v-if="resource_id === 'new'">{{ $t('resource.new') }}</div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
{{ $t('form.general') }}
</a>
</li>
<li v-if="resource_id !== 'new'" class="nav-item">
<a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
<form @submit.prevent="submit" class="card-body">
<div v-if="resource_id !== 'new'" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span :class="$root.statusClass(resource) + ' form-control-plaintext'" id="status">{{ $root.statusText(resource) }}</span>
</div>
</div>
<div class="row mb-3">
<label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="name" v-model="resource.name">
</div>
</div>
<div v-if="domains.length" class="row mb-3">
<label for="domain" class="col-sm-4 col-form-label">{{ $t('form.domain') }}</label>
<div class="col-sm-8">
<select class="form-select" v-model="resource.domain">
<option v-for="_domain in domains" :key="_domain.id" :value="_domain.namespace">{{ _domain.namespace }}</option>
</select>
</div>
</div>
<div v-if="resource.email" class="row mb-3">
<label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="email" disabled v-model="resource.email">
</div>
</div>
<btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
</form>
</div>
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<form @submit.prevent="submitSettings" class="card-body">
<div class="row mb-3">
<label for="invitation_policy" class="col-sm-4 col-form-label">{{ $t('resource.invitation-policy') }}</label>
<div class="col-sm-8">
<div class="input-group input-group-select mb-1">
<select class="form-select" id="invitation_policy" v-model="resource.config.invitation_policy" @change="policyChange">
<option value="accept">{{ $t('resource.ipolicy-accept') }}</option>
<option value="manual">{{ $t('resource.ipolicy-manual') }}</option>
<option value="reject">{{ $t('resource.ipolicy-reject') }}</option>
</select>
<input type="text" class="form-control" id="owner" v-model="resource.config.owner" :placeholder="$t('form.email')">
</div>
<small id="invitation-policy-hint" class="text-muted">
{{ $t('resource.invitation-policy-text') }}
</small>
</div>
</div>
<btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import StatusComponent from '../Widgets/Status'
export default {
components: {
StatusComponent
},
data() {
return {
domains: [],
resource_id: null,
resource: { config: {} },
status: {}
}
},
created() {
this.resource_id = this.$route.params.resource
if (this.resource_id != 'new') {
axios.get('/api/v4/resources/' + this.resource_id, { loader: true })
.then(response => {
this.resource = response.data
this.status = response.data.statusInfo
if (this.resource.config.invitation_policy.match(/^manual:(.+)$/)) {
this.resource.config.owner = RegExp.$1
this.resource.config.invitation_policy = 'manual'
}
this.$nextTick().then(() => { this.policyChange() })
})
.catch(this.$root.errorHandler)
} else {
axios.get('/api/v4/domains', { loader: true })
.then(response => {
- this.domains = response.data
+ this.domains = response.data.list
this.resource.domain = this.domains[0].namespace
})
.catch(this.$root.errorHandler)
}
},
mounted() {
$('#name').focus()
},
methods: {
deleteResource() {
axios.delete('/api/v4/resources/' + this.resource_id)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.$router.push({ name: 'resources' })
}
})
},
policyChange() {
let select = $('#invitation_policy')
select.parent()[select.val() == 'manual' ? 'addClass' : 'removeClass']('selected')
},
statusUpdate(resource) {
this.resource = Object.assign({}, this.resource, resource)
},
submit() {
this.$root.clearFormValidation($('#resource-info form'))
let method = 'post'
let location = '/api/v4/resources'
if (this.resource_id !== 'new') {
method = 'put'
location += '/' + this.resource_id
}
const post = this.$root.pick(this.resource, ['id', 'name', 'domain'])
axios[method](location, post)
.then(response => {
this.$toast.success(response.data.message)
this.$router.push({ name: 'resources' })
})
},
submitSettings() {
this.$root.clearFormValidation($('#settings form'))
let post = this.$root.pick(this.resource.config, ['invitation_policy', 'owner'])
if (post.invitation_policy == 'manual') {
post.invitation_policy += ':' + post.owner
}
delete post.owner
axios.post('/api/v4/resources/' + this.resource_id + '/config', post)
.then(response => {
this.$toast.success(response.data.message)
})
}
}
}
</script>
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 @@
<template>
<div class="container">
<div class="card" id="resource-list">
<div class="card-body">
<div class="card-title">
{{ $tc('resource.list-title', 2) }}
<small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
<btn-router v-if="!$root.isDegraded()" to="resource/new" class="btn-success float-end" icon="gear">
{{ $t('resource.create') }}
</btn-router>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="resource in resources" :key="resource.id" @click="$root.clickRecord">
<td>
<svg-icon icon="gear" :class="$root.statusClass(resource)" :title="$root.statusText(resource)"></svg-icon>
<router-link :to="{ path: 'resource/' + resource.id }">{{ resource.name }}</router-link>
</td>
<td>
<router-link :to="{ path: 'resource/' + resource.id }">{{ resource.email }}</router-link>
</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('resource.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
require('@fortawesome/free-solid-svg-icons/faGear').definition,
)
export default {
data() {
return {
resources: []
}
},
created() {
axios.get('/api/v4/resources', { loader: true })
.then(response => {
- this.resources = response.data
+ this.resources = response.data.list
})
.catch(this.$root.errorHandler)
}
}
</script>
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 @@
<template>
<div class="container">
<status-component v-if="folder_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
<div class="card" id="folder-info">
<div class="card-body">
<div class="card-title" v-if="folder_id !== 'new'">
{{ $tc('shf.list-title', 1) }}
<btn class="btn-outline-danger button-delete float-end" @click="deleteFolder()" icon="trash-can">{{ $t('shf.delete') }}</btn>
</div>
<div class="card-title" v-if="folder_id === 'new'">{{ $t('shf.new') }}</div>
<div class="card-text">
<ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-general" href="#general" role="tab" aria-controls="general" aria-selected="true" @click="$root.tab">
{{ $t('form.general') }}
</a>
</li>
<li v-if="folder_id !== 'new'" class="nav-item">
<a class="nav-link" id="tab-settings" href="#settings" role="tab" aria-controls="settings" aria-selected="false" @click="$root.tab">
{{ $t('form.settings') }}
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
<form @submit.prevent="submit" class="card-body">
<div v-if="folder_id !== 'new'" class="row plaintext mb-3">
<label for="status" class="col-sm-4 col-form-label">{{ $t('form.status') }}</label>
<div class="col-sm-8">
<span :class="$root.statusClass(folder) + ' form-control-plaintext'" id="status">{{ $root.statusText(folder) }}</span>
</div>
</div>
<div class="row mb-3">
<label for="name" class="col-sm-4 col-form-label">{{ $t('form.name') }}</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="name" v-model="folder.name">
</div>
</div>
<div class="row mb-3">
<label for="type" class="col-sm-4 col-form-label">{{ $t('form.type') }}</label>
<div class="col-sm-8">
<select id="type" class="form-select" v-model="folder.type" :disabled="folder_id !== 'new'">
<option v-for="type in types" :key="type" :value="type">{{ $t('shf.type-' + type) }}</option>
</select>
</div>
</div>
<div v-if="domains.length" class="row mb-3">
<label for="domain" class="col-sm-4 col-form-label">{{ $t('form.domain') }}</label>
<div v-if="domains.length" class="col-sm-8">
<select class="form-select" v-model="folder.domain">
<option v-for="_domain in domains" :key="_domain.id" :value="_domain.namespace">{{ _domain.namespace }}</option>
</select>
</div>
</div>
<div class="row mb-3" v-if="folder.type == 'mail'">
<label for="aliases-input" class="col-sm-4 col-form-label">{{ $t('form.emails') }}</label>
<div class="col-sm-8">
<list-input id="aliases" :list="folder.aliases"></list-input>
</div>
</div>
<btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
</form>
</div>
<div class="tab-pane" id="settings" role="tabpanel" aria-labelledby="tab-settings">
<form @submit.prevent="submitSettings" class="card-body">
<div class="row mb-3">
<label for="acl-input" class="col-sm-4 col-form-label">{{ $t('form.acl') }}</label>
<div class="col-sm-8">
<acl-input id="acl" v-model="folder.config.acl" :list="folder.config.acl" class="mb-1"></acl-input>
<small id="acl-hint" class="text-muted">
{{ $t('shf.acl-text') }}
</small>
</div>
</div>
<btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import AclInput from '../Widgets/AclInput'
import ListInput from '../Widgets/ListInput'
import StatusComponent from '../Widgets/Status'
export default {
components: {
AclInput,
ListInput,
StatusComponent
},
data() {
return {
domains: [],
folder_id: null,
folder: { type: 'mail', config: {}, aliases: [] },
status: {},
types: [ 'mail', 'event', 'task', 'contact', 'note', 'file' ]
}
},
created() {
this.folder_id = this.$route.params.folder
if (this.folder_id != 'new') {
axios.get('/api/v4/shared-folders/' + this.folder_id, { loader: true })
.then(response => {
this.folder = response.data
this.status = response.data.statusInfo
})
.catch(this.$root.errorHandler)
} else {
axios.get('/api/v4/domains', { loader: true })
.then(response => {
- this.domains = response.data
+ this.domains = response.data.list
this.folder.domain = this.domains[0].namespace
})
.catch(this.$root.errorHandler)
}
},
mounted() {
$('#name').focus()
},
methods: {
deleteFolder() {
axios.delete('/api/v4/shared-folders/' + this.folder_id)
.then(response => {
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
this.$router.push({ name: 'shared-folders' })
}
})
},
statusUpdate(folder) {
this.folder = Object.assign({}, this.folder, folder)
},
submit() {
this.$root.clearFormValidation($('#folder-info form'))
let method = 'post'
let location = '/api/v4/shared-folders'
if (this.folder_id !== 'new') {
method = 'put'
location += '/' + this.folder_id
}
const post = this.$root.pick(this.folder, ['id', 'name', 'domain', 'type', 'aliases'])
if (post.type != 'mail') {
delete post.aliases
}
axios[method](location, post)
.then(response => {
this.$toast.success(response.data.message)
this.$router.push({ name: 'shared-folders' })
})
},
submitSettings() {
this.$root.clearFormValidation($('#settings form'))
let post = this.$root.pick(this.folder.config, ['acl'])
axios.post('/api/v4/shared-folders/' + this.folder_id + '/config', post)
.then(response => {
this.$toast.success(response.data.message)
})
}
}
}
</script>
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 @@
<template>
<div class="container">
<div class="card" id="folder-list">
<div class="card-body">
<div class="card-title">
{{ $tc('shf.list-title', 2) }}
<small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
<btn-router v-if="!$root.isDegraded()" to="shared-folder/new" class="btn-success float-end" icon="gear">
{{ $t('shf.create') }}
</btn-router>
</div>
<div class="card-text">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.type') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="folder in folders" :key="folder.id" @click="$root.clickRecord">
<td>
<svg-icon icon="folder-open" :class="$root.statusClass(folder)" :title="$root.statusText(folder)"></svg-icon>
<router-link :to="{ path: 'shared-folder/' + folder.id }">{{ folder.name }}</router-link>
</td>
<td>{{ $t('shf.type-' + folder.type) }}</td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
<td colspan="2">{{ $t('shf.list-empty') }}</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
require('@fortawesome/free-solid-svg-icons/faFolderOpen').definition,
require('@fortawesome/free-solid-svg-icons/faGear').definition,
)
export default {
data() {
return {
folders: []
}
},
created() {
axios.get('/api/v4/shared-folders', { loader: true })
.then(response => {
- this.folders = response.data
+ this.folders = response.data.list
})
.catch(this.$root.errorHandler)
}
}
</script>
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 @@
<?php
namespace Tests\Feature\Controller;
use App\Domain;
use App\Entitlement;
use App\Sku;
use App\User;
use App\Wallet;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
class DomainsTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestUser('test1@' . \config('app.domain'));
$this->deleteTestUser('test2@' . \config('app.domain'));
$this->deleteTestUser('test1@domainscontroller.com');
$this->deleteTestDomain('domainscontroller.com');
}
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/<id>)
*/
public function testDestroy(): void
{
Queue::fake();
$sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$john = $this->getTestUser('john@kolab.org');
$johns_domain = $this->getTestDomain('kolab.org');
$user1 = $this->getTestUser('test1@' . \config('app.domain'));
$user2 = $this->getTestUser('test2@' . \config('app.domain'));
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
Entitlement::create([
'wallet_id' => $user1->wallets()->first()->id,
'sku_id' => $sku_domain->id,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class
]);
// Not authorized access
$response = $this->actingAs($john)->delete("api/v4/domains/{$domain->id}");
$response->assertStatus(403);
// Can't delete non-empty domain
$response = $this->actingAs($john)->delete("api/v4/domains/{$johns_domain->id}");
$response->assertStatus(422);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertEquals('error', $json['status']);
$this->assertEquals('Unable to delete a domain with assigned users or other objects.', $json['message']);
// Successful deletion
$response = $this->actingAs($user1)->delete("api/v4/domains/{$domain->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertEquals('success', $json['status']);
$this->assertEquals('Domain deleted successfully.', $json['message']);
$this->assertTrue($domain->fresh()->trashed());
// Authorized access by additional account controller
$this->deleteTestDomain('domainscontroller.com');
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
Entitlement::create([
'wallet_id' => $user1->wallets()->first()->id,
'sku_id' => $sku_domain->id,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class
]);
$user1->wallets()->first()->addController($user2);
$response = $this->actingAs($user2)->delete("api/v4/domains/{$domain->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertEquals('success', $json['status']);
$this->assertEquals('Domain deleted successfully.', $json['message']);
$this->assertTrue($domain->fresh()->trashed());
}
/**
* Test fetching domains list
*/
public function testIndex(): void
{
// User with no domains
$user = $this->getTestUser('test1@domainscontroller.com');
$response = $this->actingAs($user)->get("api/v4/domains");
$response->assertStatus(200);
$json = $response->json();
- $this->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/<domain>/config)
*/
public function testSetConfig(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$domain = $this->getTestDomain('kolab.org');
$domain->setSetting('spf_whitelist', null);
// Test unknown domain id
$post = ['spf_whitelist' => []];
$response = $this->actingAs($john)->post("/api/v4/domains/123/config", $post);
$json = $response->json();
$response->assertStatus(404);
// Test access by user not being a wallet controller
$post = ['spf_whitelist' => []];
$response = $this->actingAs($jack)->post("/api/v4/domains/{$domain->id}/config", $post);
$json = $response->json();
$response->assertStatus(403);
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['grey' => 1];
$response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertCount(1, $json['errors']);
$this->assertSame('The requested configuration parameter is not supported.', $json['errors']['grey']);
$this->assertNull($domain->fresh()->getSetting('spf_whitelist'));
// Test some valid data
$post = ['spf_whitelist' => ['.test.domain.com']];
$response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('Domain settings updated successfully.', $json['message']);
$expected = \json_encode($post['spf_whitelist']);
$this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist'));
// Test input validation
$post = ['spf_whitelist' => ['aaa']];
$response = $this->actingAs($john)->post("/api/v4/domains/{$domain->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertSame(
'The entry format is invalid. Expected a domain name starting with a dot.',
$json['errors']['spf_whitelist'][0]
);
$this->assertSame($expected, $domain->fresh()->getSetting('spf_whitelist'));
}
/**
* Test fetching domain info
*/
public function testShow(): void
{
$sku_domain = Sku::withEnvTenantContext()->where('title', 'domain-hosting')->first();
$user = $this->getTestUser('test1@domainscontroller.com');
$domain = $this->getTestDomain('domainscontroller.com', [
'status' => Domain::STATUS_NEW,
'type' => Domain::TYPE_EXTERNAL,
]);
$discount = \App\Discount::withEnvTenantContext()->where('code', 'TEST')->first();
$wallet = $user->wallet();
$wallet->discount()->associate($discount);
$wallet->save();
Entitlement::create([
'wallet_id' => $user->wallets()->first()->id,
'sku_id' => $sku_domain->id,
'entitleable_id' => $domain->id,
'entitleable_type' => Domain::class
]);
$response = $this->actingAs($user)->get("api/v4/domains/{$domain->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertEquals($domain->id, $json['id']);
$this->assertEquals($domain->namespace, $json['namespace']);
$this->assertEquals($domain->status, $json['status']);
$this->assertEquals($domain->type, $json['type']);
$this->assertSame($domain->hash(Domain::HASH_TEXT), $json['hash_text']);
$this->assertSame($domain->hash(Domain::HASH_CNAME), $json['hash_cname']);
$this->assertSame($domain->hash(Domain::HASH_CODE), $json['hash_code']);
$this->assertSame([], $json['config']['spf_whitelist']);
$this->assertCount(4, $json['mx']);
$this->assertTrue(strpos(implode("\n", $json['mx']), $domain->namespace) !== false);
$this->assertCount(8, $json['dns']);
$this->assertTrue(strpos(implode("\n", $json['dns']), $domain->namespace) !== false);
$this->assertTrue(strpos(implode("\n", $json['dns']), $domain->hash()) !== false);
$this->assertTrue(is_array($json['statusInfo']));
// Values below are tested by Unit tests
$this->assertArrayHasKey('isConfirmed', $json);
$this->assertArrayHasKey('isDeleted', $json);
$this->assertArrayHasKey('isVerified', $json);
$this->assertArrayHasKey('isSuspended', $json);
$this->assertArrayHasKey('isActive', $json);
$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/<domain-id>/status)
* and forcing setup process update (?refresh=1)
*
* @group dns
*/
public function testStatus(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$domain = $this->getTestDomain('kolab.org');
// Test unauthorized access
$response = $this->actingAs($jack)->get("/api/v4/domains/{$domain->id}/status");
$response->assertStatus(403);
$domain->status = Domain::STATUS_NEW | Domain::STATUS_ACTIVE | Domain::STATUS_LDAP_READY;
$domain->save();
// Get domain status
$response = $this->actingAs($john)->get("/api/v4/domains/{$domain->id}/status");
$response->assertStatus(200);
$json = $response->json();
$this->assertFalse($json['isVerified']);
$this->assertFalse($json['isReady']);
$this->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 @@
<?php
namespace Tests\Feature\Controller;
use App\Group;
use App\Http\Controllers\API\V4\GroupsController;
use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class GroupsTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestGroup('group-test@kolab.org');
$this->deleteTestGroup('group-test2@kolab.org');
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestGroup('group-test@kolab.org');
$this->deleteTestGroup('group-test2@kolab.org');
parent::tearDown();
}
/**
* Test group deleting (DELETE /api/v4/groups/<id>)
*/
public function testDestroy(): void
{
// First create some groups to delete
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
// Test unauth access
$response = $this->delete("api/v4/groups/{$group->id}");
$response->assertStatus(401);
// Test non-existing group
$response = $this->actingAs($john)->delete("api/v4/groups/abc");
$response->assertStatus(404);
// Test access to other user's group
$response = $this->actingAs($jack)->delete("api/v4/groups/{$group->id}");
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test removing a group
$response = $this->actingAs($john)->delete("api/v4/groups/{$group->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertEquals('success', $json['status']);
$this->assertEquals("Distribution list deleted successfully.", $json['message']);
}
/**
* Test groups listing (GET /api/v4/groups)
*/
public function testIndex(): void
{
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
// Test unauth access
$response = $this->get("api/v4/groups");
$response->assertStatus(401);
// Test a user with no groups
$response = $this->actingAs($jack)->get("/api/v4/groups");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(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/<group>/config)
*/
public function testSetConfig(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
// Test unknown group id
$post = ['sender_policy' => []];
$response = $this->actingAs($john)->post("/api/v4/groups/123/config", $post);
$json = $response->json();
$response->assertStatus(404);
// Test access by user not being a wallet controller
$post = ['sender_policy' => []];
$response = $this->actingAs($jack)->post("/api/v4/groups/{$group->id}/config", $post);
$json = $response->json();
$response->assertStatus(403);
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['test' => 1];
$response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertCount(1, $json['errors']);
$this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']);
$group->refresh();
$this->assertNull($group->getSetting('test'));
$this->assertNull($group->getSetting('sender_policy'));
// Test some valid data
$post = ['sender_policy' => ['domain.com']];
$response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertSame('Distribution list settings updated successfully.', $json['message']);
$this->assertSame(['sender_policy' => $post['sender_policy']], $group->fresh()->getConfig());
// Test input validation
$post = ['sender_policy' => [5]];
$response = $this->actingAs($john)->post("/api/v4/groups/{$group->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertSame(
'The entry format is invalid. Expected an email, domain, or part of it.',
$json['errors']['sender_policy'][0]
);
$this->assertSame(['sender_policy' => ['domain.com']], $group->fresh()->getConfig());
}
/**
* Test fetching group data/profile (GET /api/v4/groups/<group-id>)
*/
public function testShow(): void
{
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
$group->setSetting('sender_policy', '["test"]');
// Test unauthorized access to a profile of other user
$response = $this->get("/api/v4/groups/{$group->id}");
$response->assertStatus(401);
// Test unauthorized access to a group of another user
$response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}");
$response->assertStatus(403);
// John: Group owner - non-existing group
$response = $this->actingAs($john)->get("/api/v4/groups/abc");
$response->assertStatus(404);
// John: Group owner
$response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame($group->id, $json['id']);
$this->assertSame($group->email, $json['email']);
$this->assertSame($group->name, $json['name']);
$this->assertSame($group->members, $json['members']);
$this->assertTrue(!empty($json['statusInfo']));
$this->assertArrayHasKey('isDeleted', $json);
$this->assertArrayHasKey('isSuspended', $json);
$this->assertArrayHasKey('isActive', $json);
$this->assertArrayHasKey('isLdapReady', $json);
$this->assertSame(['sender_policy' => ['test']], $json['config']);
}
/**
* Test fetching group status (GET /api/v4/groups/<group-id>/status)
* and forcing setup process update (?refresh=1)
*/
public function testStatus(): void
{
Queue::fake();
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
// Test unauthorized access
$response = $this->get("/api/v4/groups/abc/status");
$response->assertStatus(401);
// Test unauthorized access
$response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}/status");
$response->assertStatus(403);
$group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE;
$group->save();
// Get group status
$response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status");
$response->assertStatus(200);
$json = $response->json();
$this->assertFalse($json['isLdapReady']);
$this->assertFalse($json['isReady']);
$this->assertFalse($json['isSuspended']);
$this->assertTrue($json['isActive']);
$this->assertFalse($json['isDeleted']);
$this->assertCount(6, $json['process']);
$this->assertSame('distlist-new', $json['process'][0]['label']);
$this->assertSame(true, $json['process'][0]['state']);
$this->assertSame('distlist-ldap-ready', $json['process'][1]['label']);
$this->assertSame(false, $json['process'][1]['state']);
$this->assertTrue(empty($json['status']));
$this->assertTrue(empty($json['message']));
// Make sure the domain is confirmed (other test might unset that status)
$domain = $this->getTestDomain('kolab.org');
$domain->status |= \App\Domain::STATUS_CONFIRMED;
$domain->save();
// Now "reboot" the process and the group
$response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status?refresh=1");
$response->assertStatus(200);
$json = $response->json();
$this->assertTrue($json['isLdapReady']);
$this->assertTrue($json['isReady']);
$this->assertCount(6, $json['process']);
$this->assertSame('distlist-ldap-ready', $json['process'][1]['label']);
$this->assertSame(true, $json['process'][1]['state']);
$this->assertSame('success', $json['status']);
$this->assertSame('Setup process finished successfully.', $json['message']);
// Test a case when a domain is not ready
$domain->status ^= \App\Domain::STATUS_CONFIRMED;
$domain->save();
$response = $this->actingAs($john)->get("/api/v4/groups/{$group->id}/status?refresh=1");
$response->assertStatus(200);
$json = $response->json();
$this->assertTrue($json['isLdapReady']);
$this->assertTrue($json['isReady']);
$this->assertCount(6, $json['process']);
$this->assertSame('distlist-ldap-ready', $json['process'][1]['label']);
$this->assertSame(true, $json['process'][1]['state']);
$this->assertSame('success', $json['status']);
$this->assertSame('Setup process finished successfully.', $json['message']);
}
/**
* Test GroupsController::statusInfo()
*/
public function testStatusInfo(): void
{
$john = $this->getTestUser('john@kolab.org');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
$group->status = Group::STATUS_NEW | Group::STATUS_ACTIVE;
$group->save();
$result = GroupsController::statusInfo($group);
$this->assertFalse($result['isReady']);
$this->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/<group-id>)
*/
public function testUpdate(): void
{
Queue::fake();
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$group = $this->getTestGroup('group-test@kolab.org');
$group->assignToWallet($john->wallets->first());
// Test unauthorized update
$response = $this->get("/api/v4/groups/{$group->id}", []);
$response->assertStatus(401);
// Test unauthorized update
$response = $this->actingAs($jack)->get("/api/v4/groups/{$group->id}", []);
$response->assertStatus(403);
// Test updating - missing members
$response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", []);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertSame("At least one recipient is required.", $json['errors']['members']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['members' => ['test@domain.tld', 'invalid']];
$response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertSame('The specified email address is invalid.', $json['errors']['members'][1]);
// Valid data - members and name changed
$post = [
'name' => 'Test Gr',
'members' => ['member1@test.domain', 'member2@test.domain']
];
$response = $this->actingAs($john)->put("/api/v4/groups/{$group->id}", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("Distribution list updated successfully.", $json['message']);
$this->assertCount(2, $json);
$group->refresh();
$this->assertSame($post['name'], $group->name);
$this->assertSame($post['members'], $group->members);
}
/**
* Group email address validation.
*/
public function testValidateGroupEmail(): void
{
$john = $this->getTestUser('john@kolab.org');
$group = $this->getTestGroup('group-test@kolab.org');
// Invalid email
$result = GroupsController::validateGroupEmail('', $john);
$this->assertSame("The email field is required.", $result);
$result = GroupsController::validateGroupEmail('kolab.org', $john);
$this->assertSame("The specified email is invalid.", $result);
$result = GroupsController::validateGroupEmail('.@kolab.org', $john);
$this->assertSame("The specified email is invalid.", $result);
$result = GroupsController::validateGroupEmail('test123456@localhost', $john);
$this->assertSame("The specified domain is invalid.", $result);
$result = GroupsController::validateGroupEmail('test123456@unknown-domain.org', $john);
$this->assertSame("The specified domain is invalid.", $result);
// forbidden public domain
$result = GroupsController::validateGroupEmail('testtest@kolabnow.com', $john);
$this->assertSame("The specified domain is not available.", $result);
// existing alias
$result = GroupsController::validateGroupEmail('jack.daniels@kolab.org', $john);
$this->assertSame("The specified email is not available.", $result);
// existing user
$result = GroupsController::validateGroupEmail('ned@kolab.org', $john);
$this->assertSame("The specified email is not available.", $result);
// existing group
$result = GroupsController::validateGroupEmail('group-test@kolab.org', $john);
$this->assertSame("The specified email is not available.", $result);
// valid
$result = GroupsController::validateGroupEmail('admin@kolab.org', $john);
$this->assertSame(null, $result);
}
/**
* Group member email address validation.
*/
public function testValidateMemberEmail(): void
{
$john = $this->getTestUser('john@kolab.org');
// Invalid format
$result = GroupsController::validateMemberEmail('kolab.org', $john);
$this->assertSame("The specified email address is invalid.", $result);
$result = GroupsController::validateMemberEmail('.@kolab.org', $john);
$this->assertSame("The specified email address is invalid.", $result);
$result = GroupsController::validateMemberEmail('test123456@localhost', $john);
$this->assertSame("The specified email address is invalid.", $result);
// Test local non-existing user
$result = GroupsController::validateMemberEmail('unknown@kolab.org', $john);
$this->assertSame("The specified email address does not exist.", $result);
// Test local existing user
$result = GroupsController::validateMemberEmail('ned@kolab.org', $john);
$this->assertSame(null, $result);
// Test existing user, but not in the same account
$result = GroupsController::validateMemberEmail('jeroen@jeroen.jeroen', $john);
$this->assertSame(null, $result);
// Valid address
$result = GroupsController::validateMemberEmail('test@google.com', $john);
$this->assertSame(null, $result);
}
}
diff --git a/src/tests/Feature/Controller/ResourcesTest.php b/src/tests/Feature/Controller/ResourcesTest.php
index da6c8aa7..7a5ccc8d 100644
--- a/src/tests/Feature/Controller/ResourcesTest.php
+++ b/src/tests/Feature/Controller/ResourcesTest.php
@@ -1,482 +1,494 @@
<?php
namespace Tests\Feature\Controller;
use App\Resource;
use App\Http\Controllers\API\V4\ResourcesController;
use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class ResourcesTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestResource('resource-test@kolab.org');
Resource::where('name', 'Test Resource')->delete();
}
/**
* {@inheritDoc}
*/
public function tearDown(): void
{
$this->deleteTestResource('resource-test@kolab.org');
Resource::where('name', 'Test Resource')->delete();
parent::tearDown();
}
/**
* Test resource deleting (DELETE /api/v4/resources/<id>)
*/
public function testDestroy(): void
{
// First create some groups to delete
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$resource = $this->getTestResource('resource-test@kolab.org');
$resource->assignToWallet($john->wallets->first());
// Test unauth access
$response = $this->delete("api/v4/resources/{$resource->id}");
$response->assertStatus(401);
// Test non-existing resource
$response = $this->actingAs($john)->delete("api/v4/resources/abc");
$response->assertStatus(404);
// Test access to other user's resource
$response = $this->actingAs($jack)->delete("api/v4/resources/{$resource->id}");
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test removing a resource
$response = $this->actingAs($john)->delete("api/v4/resources/{$resource->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertEquals('success', $json['status']);
$this->assertEquals("Resource deleted successfully.", $json['message']);
}
/**
* Test resources listing (GET /api/v4/resources)
*/
public function testIndex(): void
{
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
// Test unauth access
$response = $this->get("api/v4/resources");
$response->assertStatus(401);
// Test a user with no resources
$response = $this->actingAs($jack)->get("/api/v4/resources");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(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/<resource>/config)
*/
public function testSetConfig(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$resource = $this->getTestResource('resource-test@kolab.org');
$resource->assignToWallet($john->wallets->first());
// Test unknown resource id
$post = ['invitation_policy' => 'reject'];
$response = $this->actingAs($john)->post("/api/v4/resources/123/config", $post);
$json = $response->json();
$response->assertStatus(404);
// Test access by user not being a wallet controller
$post = ['invitation_policy' => 'reject'];
$response = $this->actingAs($jack)->post("/api/v4/resources/{$resource->id}/config", $post);
$json = $response->json();
$response->assertStatus(403);
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['test' => 1];
$response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertCount(1, $json['errors']);
$this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']);
$resource->refresh();
$this->assertNull($resource->getSetting('test'));
$this->assertNull($resource->getSetting('invitation_policy'));
// Test some valid data
$post = ['invitation_policy' => 'reject'];
$response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertSame("Resource settings updated successfully.", $json['message']);
$this->assertSame(['invitation_policy' => 'reject'], $resource->fresh()->getConfig());
// Test input validation
$post = ['invitation_policy' => 'aaa'];
$response = $this->actingAs($john)->post("/api/v4/resources/{$resource->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertSame(
"The specified invitation policy is invalid.",
$json['errors']['invitation_policy']
);
$this->assertSame(['invitation_policy' => 'reject'], $resource->fresh()->getConfig());
}
/**
* Test fetching resource data/profile (GET /api/v4/resources/<resource>)
*/
public function testShow(): void
{
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$resource = $this->getTestResource('resource-test@kolab.org');
$resource->assignToWallet($john->wallets->first());
$resource->setSetting('invitation_policy', 'reject');
// Test unauthorized access to a profile of other user
$response = $this->get("/api/v4/resources/{$resource->id}");
$response->assertStatus(401);
// Test unauthorized access to a resource of another user
$response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}");
$response->assertStatus(403);
// John: Account owner - non-existing resource
$response = $this->actingAs($john)->get("/api/v4/resources/abc");
$response->assertStatus(404);
// John: Account owner
$response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame($resource->id, $json['id']);
$this->assertSame($resource->email, $json['email']);
$this->assertSame($resource->name, $json['name']);
$this->assertTrue(!empty($json['statusInfo']));
$this->assertArrayHasKey('isDeleted', $json);
$this->assertArrayHasKey('isActive', $json);
$this->assertArrayHasKey('isLdapReady', $json);
$this->assertArrayHasKey('isImapReady', $json);
$this->assertSame(['invitation_policy' => 'reject'], $json['config']);
}
/**
* Test fetching a resource status (GET /api/v4/resources/<resource>/status)
* and forcing setup process update (?refresh=1)
*/
public function testStatus(): void
{
Queue::fake();
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$resource = $this->getTestResource('resource-test@kolab.org');
$resource->assignToWallet($john->wallets->first());
// Test unauthorized access
$response = $this->get("/api/v4/resources/abc/status");
$response->assertStatus(401);
// Test unauthorized access
$response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}/status");
$response->assertStatus(403);
$resource->status = Resource::STATUS_NEW | Resource::STATUS_ACTIVE;
$resource->save();
// Get resource status
$response = $this->actingAs($john)->get("/api/v4/resources/{$resource->id}/status");
$response->assertStatus(200);
$json = $response->json();
$this->assertFalse($json['isLdapReady']);
$this->assertFalse($json['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/<resource>)
*/
public function testUpdate(): void
{
Queue::fake();
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$resource = $this->getTestResource('resource-test@kolab.org');
$resource->assignToWallet($john->wallets->first());
// Test unauthorized update
$response = $this->get("/api/v4/resources/{$resource->id}", []);
$response->assertStatus(401);
// Test unauthorized update
$response = $this->actingAs($jack)->get("/api/v4/resources/{$resource->id}", []);
$response->assertStatus(403);
// Name change
$post = [
'name' => 'Test Res',
];
$response = $this->actingAs($john)->put("/api/v4/resources/{$resource->id}", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("Resource updated successfully.", $json['message']);
$this->assertCount(2, $json);
$resource->refresh();
$this->assertSame($post['name'], $resource->name);
}
}
diff --git a/src/tests/Feature/Controller/SharedFoldersTest.php b/src/tests/Feature/Controller/SharedFoldersTest.php
index 9518f63b..f469d054 100644
--- a/src/tests/Feature/Controller/SharedFoldersTest.php
+++ b/src/tests/Feature/Controller/SharedFoldersTest.php
@@ -1,550 +1,562 @@
<?php
namespace Tests\Feature\Controller;
use App\SharedFolder;
use App\Http\Controllers\API\V4\SharedFoldersController;
use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class SharedFoldersTest extends TestCase
{
/**
* {@inheritDoc}
*/
public function setUp(): void
{
parent::setUp();
$this->deleteTestSharedFolder('folder-test@kolab.org');
SharedFolder::where('name', '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/<id>)
*/
public function testDestroy(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$folder = $this->getTestSharedFolder('folder-test@kolab.org');
$folder->assignToWallet($john->wallets->first());
// Test unauth access
$response = $this->delete("api/v4/shared-folders/{$folder->id}");
$response->assertStatus(401);
// Test non-existing folder
$response = $this->actingAs($john)->delete("api/v4/shared-folders/abc");
$response->assertStatus(404);
// Test access to other user's folder
$response = $this->actingAs($jack)->delete("api/v4/shared-folders/{$folder->id}");
$response->assertStatus(403);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test removing a folder
$response = $this->actingAs($john)->delete("api/v4/shared-folders/{$folder->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertEquals('success', $json['status']);
$this->assertEquals("Shared folder deleted successfully.", $json['message']);
}
/**
* Test shared folders listing (GET /api/v4/shared-folders)
*/
public function testIndex(): void
{
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
// Test unauth access
$response = $this->get("api/v4/shared-folders");
$response->assertStatus(401);
// Test a user with no shared folders
$response = $this->actingAs($jack)->get("/api/v4/shared-folders");
$response->assertStatus(200);
$json = $response->json();
- $this->assertCount(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/<folder>/config)
*/
public function testSetConfig(): void
{
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$folder = $this->getTestSharedFolder('folder-test@kolab.org');
$folder->assignToWallet($john->wallets->first());
// Test unknown resource id
$post = ['acl' => ['john@kolab.org, full']];
$response = $this->actingAs($john)->post("/api/v4/shared-folders/123/config", $post);
$json = $response->json();
$response->assertStatus(404);
// Test access by user not being a wallet controller
$response = $this->actingAs($jack)->post("/api/v4/shared-folders/{$folder->id}/config", $post);
$json = $response->json();
$response->assertStatus(403);
$this->assertSame('error', $json['status']);
$this->assertSame("Access denied", $json['message']);
$this->assertCount(2, $json);
// Test some invalid data
$post = ['test' => 1];
$response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertCount(1, $json['errors']);
$this->assertSame('The requested configuration parameter is not supported.', $json['errors']['test']);
$folder->refresh();
$this->assertNull($folder->getSetting('test'));
$this->assertNull($folder->getSetting('acl'));
// Test some valid data
$post = ['acl' => ['john@kolab.org, full']];
$response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post);
$response->assertStatus(200);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('success', $json['status']);
$this->assertSame("Shared folder settings updated successfully.", $json['message']);
$this->assertSame(['acl' => $post['acl']], $folder->fresh()->getConfig());
// Test input validation
$post = ['acl' => ['john@kolab.org, full', 'test, full']];
$response = $this->actingAs($john)->post("/api/v4/shared-folders/{$folder->id}/config", $post);
$response->assertStatus(422);
$json = $response->json();
$this->assertCount(2, $json);
$this->assertSame('error', $json['status']);
$this->assertCount(1, $json['errors']);
$this->assertCount(1, $json['errors']['acl']);
$this->assertSame(
"The specified email address is invalid.",
$json['errors']['acl'][1]
);
$this->assertSame(['acl' => ['john@kolab.org, full']], $folder->fresh()->getConfig());
}
/**
* Test fetching shared folder data/profile (GET /api/v4/shared-folders/<folder>)
*/
public function testShow(): void
{
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$folder = $this->getTestSharedFolder('folder-test@kolab.org');
$folder->assignToWallet($john->wallets->first());
$folder->setSetting('acl', '["anyone, full"]');
$folder->setAliases(['folder-alias@kolab.org']);
// Test unauthenticated access
$response = $this->get("/api/v4/shared-folders/{$folder->id}");
$response->assertStatus(401);
// Test unauthorized access to a shared folder of another user
$response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}");
$response->assertStatus(403);
// John: Account owner - non-existing folder
$response = $this->actingAs($john)->get("/api/v4/shared-folders/abc");
$response->assertStatus(404);
// John: Account owner
$response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}");
$response->assertStatus(200);
$json = $response->json();
$this->assertSame($folder->id, $json['id']);
$this->assertSame($folder->email, $json['email']);
$this->assertSame($folder->name, $json['name']);
$this->assertSame($folder->type, $json['type']);
$this->assertSame(['folder-alias@kolab.org'], $json['aliases']);
$this->assertTrue(!empty($json['statusInfo']));
$this->assertArrayHasKey('isDeleted', $json);
$this->assertArrayHasKey('isActive', $json);
$this->assertArrayHasKey('isLdapReady', $json);
$this->assertArrayHasKey('isImapReady', $json);
$this->assertSame(['acl' => ['anyone, full']], $json['config']);
}
/**
* Test fetching a shared folder status (GET /api/v4/shared-folders/<folder>/status)
* and forcing setup process update (?refresh=1)
*/
public function testStatus(): void
{
Queue::fake();
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$folder = $this->getTestSharedFolder('folder-test@kolab.org');
$folder->assignToWallet($john->wallets->first());
// Test unauthorized access
$response = $this->get("/api/v4/shared-folders/abc/status");
$response->assertStatus(401);
// Test unauthorized access
$response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}/status");
$response->assertStatus(403);
$folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE;
$folder->save();
// Get resource status
$response = $this->actingAs($john)->get("/api/v4/shared-folders/{$folder->id}/status");
$response->assertStatus(200);
$json = $response->json();
$this->assertFalse($json['isLdapReady']);
$this->assertFalse($json['isImapReady']);
$this->assertFalse($json['isReady']);
$this->assertFalse($json['isDeleted']);
$this->assertTrue($json['isActive']);
$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/<folder)
*/
public function testUpdate(): void
{
Queue::fake();
$jack = $this->getTestUser('jack@kolab.org');
$john = $this->getTestUser('john@kolab.org');
$ned = $this->getTestUser('ned@kolab.org');
$folder = $this->getTestSharedFolder('folder-test@kolab.org');
$folder->assignToWallet($john->wallets->first());
// Test unauthorized update
$response = $this->get("/api/v4/shared-folders/{$folder->id}", []);
$response->assertStatus(401);
// Test unauthorized update
$response = $this->actingAs($jack)->get("/api/v4/shared-folders/{$folder->id}", []);
$response->assertStatus(403);
// Name change
$post = [
'name' => 'Test Res',
];
$response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("Shared folder updated successfully.", $json['message']);
$this->assertCount(2, $json);
$folder->refresh();
$this->assertSame($post['name'], $folder->name);
// Aliases with error
$post['aliases'] = ['folder-alias1@kolab.org', 'folder-alias2@unknown.com'];
$response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post);
$json = $response->json();
$response->assertStatus(422);
$this->assertSame('error', $json['status']);
$this->assertCount(2, $json);
$this->assertCount(1, $json['errors']);
$this->assertCount(1, $json['errors']['aliases']);
$this->assertSame("The specified domain is invalid.", $json['errors']['aliases'][1]);
$this->assertSame([], $folder->aliases()->pluck('alias')->all());
// Aliases with success expected
$post['aliases'] = ['folder-alias1@kolab.org', 'folder-alias2@kolab.org'];
$response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post);
$json = $response->json();
$response->assertStatus(200);
$this->assertSame('success', $json['status']);
$this->assertSame("Shared folder updated successfully.", $json['message']);
$this->assertCount(2, $json);
$this->assertSame($post['aliases'], $folder->aliases()->pluck('alias')->all());
// All aliases removal
$post['aliases'] = [];
$response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post);
$response->assertStatus(200);
$this->assertSame($post['aliases'], $folder->aliases()->pluck('alias')->all());
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Feb 3, 9:27 PM (13 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
427460
Default Alt Text
(147 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment