Page MenuHomePhorge

D2020.1774824860.diff
No OneTemporary

Authored By
Unknown
Size
51 KB
Referenced Files
None
Subscribers
None

D2020.1774824860.diff

diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php
--- a/src/app/Backends/LDAP.php
+++ b/src/app/Backends/LDAP.php
@@ -3,6 +3,7 @@
namespace App\Backends;
use App\Domain;
+use App\Group;
use App\User;
class LDAP
@@ -213,6 +214,18 @@
}
}
+ /**
+ * Create a group in LDAP.
+ *
+ * @param \App\Group $group The group to create.
+ *
+ * @throws \Exception
+ */
+ public static function createGroup(Group $group): void
+ {
+ // TODO
+ }
+
/**
* Create a user in LDAP.
*
@@ -279,7 +292,7 @@
/**
* Delete a domain from LDAP.
*
- * @param \App\Domain $domain The domain to update.
+ * @param \App\Domain $domain The domain to delete
*
* @throws \Exception
*/
@@ -322,10 +335,22 @@
}
}
+ /**
+ * Delete a group from LDAP.
+ *
+ * @param \App\Group $group The group to delete.
+ *
+ * @throws \Exception
+ */
+ public static function deleteGroup(Group $group): void
+ {
+ // TODO
+ }
+
/**
* Delete a user from LDAP.
*
- * @param \App\User $user The user account to update.
+ * @param \App\User $user The user account to delete.
*
* @throws \Exception
*/
@@ -442,6 +467,18 @@
}
}
+ /**
+ * Update a group in LDAP.
+ *
+ * @param \App\Group $group The group to update
+ *
+ * @throws \Exception
+ */
+ public static function updateGroup(Group $group): void
+ {
+ // TODO
+ }
+
/**
* Update a user in LDAP.
*
diff --git a/src/app/Console/Commands/Group/AddMember.php b/src/app/Console/Commands/Group/AddMember.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/AddMember.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class AddMember extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:add-member {group} {member}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Add a member to a group.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $input = $this->argument('group');
+ $member = \strtolower($this->argument('member'));
+ $group = $this->getObject(\App\Group::class, $input, 'email');
+
+ if (empty($group)) {
+ $this->error("Group {$input} does not exist.");
+ return 1;
+ }
+
+ if (in_array($member, $group->members)) {
+ $this->error("{$member}: Already exists in the group.");
+ return 1;
+ }
+
+ if ($error = Create::validateMemberEmail($member)) {
+ $this->error("{$member}: $error");
+ return 1;
+ }
+
+ // We can't modify the property indirectly, therefor array_merge()
+ $group->members = array_merge($group->members, [$member]);
+ $group->save();
+ }
+}
diff --git a/src/app/Console/Commands/Group/Create.php b/src/app/Console/Commands/Group/Create.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/Create.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+use App\Domain;
+use App\Group;
+use App\User;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Validator;
+
+class Create extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:create {email} {--user=} {--member=*}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Create a group.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $email = $this->argument('email');
+ $user = $this->option('user');
+ $members = $this->option('member');
+
+ if (empty($user)) {
+ $this->error("The --user option is required.");
+ return 1;
+ }
+
+ $owner = $this->getUser($user);
+
+ if (empty($owner)) {
+ $this->error("User {$user} does not exist.");
+ return 1;
+ }
+
+ // Validate group email address
+ foreach ($members as $i => $member) {
+ if ($error = $this->validateMemberEmail($member)) {
+ $this->error("{$member}: $error");
+ return 1;
+ }
+ if (\strtolower($member) === \strtolower($email)) {
+ $this->error("{$member}: Cannot be the same as the group address.");
+ return 1;
+ }
+ }
+
+ // Validate members addresses
+ if ($error = $this->validateGroupEmail($email, $owner)) {
+ $this->error("{$email}: $error");
+ return 1;
+ }
+
+ DB::beginTransaction();
+
+ // Create the group
+ $group = new Group();
+ $group->email = $email;
+ $group->members = $members;
+ $group->save();
+
+ $group->assignToWallet($owner->wallets->first());
+
+ DB::commit();
+
+ $this->info($group->id);
+ }
+
+ /**
+ * Validate an email address for use as a group member
+ *
+ * @param string $email Email address
+ *
+ * @return ?string Error message on validation error
+ */
+ public static function validateMemberEmail(string $email): ?string
+ {
+ $v = Validator::make(
+ ['email' => $email],
+ ['email' => [new \App\Rules\ExternalEmail()]]
+ );
+
+ if ($v->fails()) {
+ return $v->errors()->toArray()['email'][0];
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate an email address for use as a group email
+ *
+ * @param string $email Email address
+ * @param \App\User $user The group owner
+ *
+ * @return ?string Error message on validation error
+ */
+ public static function validateGroupEmail(string $email, \App\User $user): ?string
+ {
+ if (strpos($email, '@') === false) {
+ return \trans('validation.entryinvalid', ['attribute' => 'email']);
+ }
+
+ list($login, $domain) = explode('@', \strtolower($email));
+
+ if (strlen($login) === 0 || strlen($domain) === 0) {
+ return \trans('validation.entryinvalid', ['attribute' => 'email']);
+ }
+
+ // Check if domain exists
+ $domain = Domain::where('namespace', $domain)->first();
+
+ if (empty($domain)) {
+ return \trans('validation.domaininvalid');
+ }
+
+ // Validate login part alone
+ $v = Validator::make(
+ ['email' => $login],
+ ['email' => [new \App\Rules\UserEmailLocal(!$domain->isPublic())]]
+ );
+
+ if ($v->fails()) {
+ return $v->errors()->toArray()['email'][0];
+ }
+
+ // Check if it is one of domains available to the user
+ $domains = \collect($user->domains())->pluck('namespace')->all();
+
+ if (!in_array($domain->namespace, $domains)) {
+ // return \trans('validation.entryexists', ['attribute' => 'domain']);
+ return "Domain not available.";
+ }
+
+ // Check if a user with specified address already exists
+ if (User::emailExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+
+ // Check if an alias with specified address already exists.
+ if (User::aliasExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+
+ if (Group::emailExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+
+ return null;
+ }
+}
diff --git a/src/app/Console/Commands/Group/Delete.php b/src/app/Console/Commands/Group/Delete.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/Delete.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class Delete extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:delete {group}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Delete a group.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $input = $this->argument('group');
+ $group = $this->getObject(\App\Group::class, $input, 'email');
+
+ if (empty($group)) {
+ $this->error("Group {$input} does not exist.");
+ return 1;
+ }
+
+ $group->delete();
+ }
+}
diff --git a/src/app/Console/Commands/Group/Info.php b/src/app/Console/Commands/Group/Info.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/Info.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class Info extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:info {group}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Print a group information.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $input = $this->argument('group');
+ $group = $this->getObject(\App\Group::class, $input, 'email');
+
+ if (empty($group)) {
+ $this->error("Group {$input} does not exist.");
+ return 1;
+ }
+
+ $this->info('Id: ' . $group->id);
+ $this->info('Email: ' . $group->email);
+ $this->info('Status: ' . $group->status);
+
+ // TODO: Print owner/wallet
+
+ foreach ($group->members as $member) {
+ $this->info('Member: ' . $member);
+ }
+ }
+}
diff --git a/src/app/Console/Commands/Group/RemoveMember.php b/src/app/Console/Commands/Group/RemoveMember.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Group/RemoveMember.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace App\Console\Commands\Group;
+
+use App\Console\Command;
+
+class RemoveMember extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'group:remove-member {group} {member}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Remove a member from a group.";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $input = $this->argument('group');
+ $member = \strtolower($this->argument('member'));
+
+ $group = $this->getObject(\App\Group::class, $input, 'email');
+
+ if (empty($group)) {
+ $this->error("Group {$input} does not exist.");
+ return 1;
+ }
+
+ $members = [];
+
+ foreach ($group->members as $m) {
+ if ($m !== $member) {
+ $members[] = $m;
+ }
+ }
+
+ if (count($members) == count($group->members)) {
+ $this->error("Member {$member} not found in the group.");
+ return 1;
+ }
+
+ $group->members = $members;
+ $group->save();
+ }
+}
diff --git a/src/app/Group.php b/src/app/Group.php
new file mode 100644
--- /dev/null
+++ b/src/app/Group.php
@@ -0,0 +1,280 @@
+<?php
+
+namespace App;
+
+use App\Wallet;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+/**
+ * The eloquent definition of a Group.
+ *
+ * @property int $id The group identifier
+ * @property string $email An email address
+ * @property string $members A comma-separated list of email addresses
+ * @property int $status The group status
+ */
+class Group extends Model
+{
+ use SoftDeletes;
+
+ // we've simply never heard of this domain
+ public const STATUS_NEW = 1 << 0;
+ // it's been activated
+ public const STATUS_ACTIVE = 1 << 1;
+ // domain has been suspended.
+ public const STATUS_SUSPENDED = 1 << 2;
+ // domain has been deleted
+ public const STATUS_DELETED = 1 << 3;
+ // domain has been created in LDAP
+ public const STATUS_LDAP_READY = 1 << 4;
+
+ public $incrementing = false;
+
+ protected $keyType = 'bigint';
+
+ protected $fillable = [
+ 'email',
+ 'status',
+ 'members'
+ ];
+
+ /**
+ * Assign the group to a wallet.
+ *
+ * @param \App\Wallet $wallet The wallet
+ *
+ * @return \App\Group Self
+ * @throws \Exception
+ */
+ public function assignToWallet(Wallet $wallet): Group
+ {
+ if (empty($this->id)) {
+ throw new \Exception("Group not yet exists");
+ }
+
+ if (!empty($this->entitlement)) {
+ throw new \Exception("Group already assigned to a wallet");
+ }
+
+ $sku = \App\Sku::where('title', 'group')->first();
+ $exists = $wallet->entitlements()->where('sku_id', $sku->id)->count();
+
+ \App\Entitlement::create([
+ 'wallet_id' => $wallet->id,
+ 'sku_id' => $sku->id,
+ 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
+ 'entitleable_id' => $this->id,
+ 'entitleable_type' => Group::class
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Find whether an email address exists as a group (including deleted groups).
+ *
+ * @param string $email Email address
+ * @param bool $return_group Return Group instance instead of boolean
+ *
+ * @return \App\Group|bool True or Group model object if found, False otherwise
+ */
+ public static function emailExists(string $email, bool $return_group = false)
+ {
+ if (strpos($email, '@') === false) {
+ return false;
+ }
+
+ $email = \strtolower($email);
+
+ $group = self::withTrashed()->where('email', $email)->first();
+
+ if ($group) {
+ return $return_group ? $group : true;
+ }
+
+ return false;
+ }
+
+ /**
+ * The group entitlement.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\MorphOne
+ */
+ public function entitlement()
+ {
+ return $this->morphOne('App\Entitlement', 'entitleable');
+ }
+
+ /**
+ * Group members propert accessor. Converts internal comma-separated list into an array
+ *
+ * @param string $members Comma-separated list of email addresses
+ *
+ * @return array Email addresses of the group members, as an array
+ */
+ public function getMembersAttribute($members): array
+ {
+ return $members ? explode(',', $members) : [];
+ }
+
+ /**
+ * Returns whether this domain is active.
+ *
+ * @return bool
+ */
+ public function isActive(): bool
+ {
+ return ($this->status & self::STATUS_ACTIVE) > 0;
+ }
+
+ /**
+ * Returns whether this domain is deleted.
+ *
+ * @return bool
+ */
+ public function isDeleted(): bool
+ {
+ return ($this->status & self::STATUS_DELETED) > 0;
+ }
+
+ /**
+ * Returns whether this domain is new.
+ *
+ * @return bool
+ */
+ public function isNew(): bool
+ {
+ return ($this->status & self::STATUS_NEW) > 0;
+ }
+
+ /**
+ * Returns whether this domain is registered in LDAP.
+ *
+ * @return bool
+ */
+ public function isLdapReady(): bool
+ {
+ return ($this->status & self::STATUS_LDAP_READY) > 0;
+ }
+
+ /**
+ * Returns whether this domain is suspended.
+ *
+ * @return bool
+ */
+ public function isSuspended(): bool
+ {
+ return ($this->status & self::STATUS_SUSPENDED) > 0;
+ }
+
+ /**
+ * Ensure the email is appropriately cased.
+ *
+ * @param string $email Group email address
+ */
+ public function setEmailAttribute(string $email)
+ {
+ $this->attributes['email'] = strtolower($email);
+ }
+
+ /**
+ * Ensure the members are apropriately formatted.
+ *
+ * @param array $members Email addresses of the group members
+ */
+ public function setMembersAttribute(array $members): void
+ {
+ $members = array_filter(array_map('strtolower', $members));
+ $this->attributes['members'] = implode(',', $members);
+ }
+
+ /**
+ * Group status mutator
+ *
+ * @throws \Exception
+ */
+ public function setStatusAttribute($status)
+ {
+ $new_status = 0;
+
+ $allowed_values = [
+ self::STATUS_NEW,
+ self::STATUS_ACTIVE,
+ self::STATUS_SUSPENDED,
+ self::STATUS_DELETED,
+ self::STATUS_LDAP_READY,
+ ];
+
+ foreach ($allowed_values as $value) {
+ if ($status & $value) {
+ $new_status |= $value;
+ $status ^= $value;
+ }
+ }
+
+ if ($status > 0) {
+ throw new \Exception("Invalid group status: {$status}");
+ }
+
+ if ($new_status & self::STATUS_DELETED && $new_status & self::STATUS_ACTIVE) {
+ $new_status ^= self::STATUS_ACTIVE;
+ }
+
+ if ($new_status & self::STATUS_SUSPENDED && $new_status & self::STATUS_ACTIVE) {
+ $new_status ^= self::STATUS_ACTIVE;
+ }
+
+ // if the domain is now active, it is not new anymore.
+ if ($new_status & self::STATUS_ACTIVE && $new_status & self::STATUS_NEW) {
+ $new_status ^= self::STATUS_NEW;
+ }
+
+ $this->attributes['status'] = $new_status;
+ }
+
+ /**
+ * Suspend this group.
+ *
+ * @return void
+ */
+ public function suspend(): void
+ {
+ if ($this->isSuspended()) {
+ return;
+ }
+
+ $this->status |= Group::STATUS_SUSPENDED;
+ $this->save();
+ }
+
+ /**
+ * Unsuspend this group.
+ *
+ * @return void
+ */
+ public function unsuspend(): void
+ {
+ if (!$this->isSuspended()) {
+ return;
+ }
+
+ $this->status ^= Group::STATUS_SUSPENDED;
+ $this->status |= Group::STATUS_ACTIVE;
+
+ $this->save();
+ }
+
+ /**
+ * Returns the wallet by which the group is controlled
+ *
+ * @return \App\Wallet A wallet object
+ */
+ public function wallet(): ?Wallet
+ {
+ // Note: Not all domains have a entitlement/wallet
+ $entitlement = $this->entitlement()->withTrashed()->first();
+
+ return $entitlement ? $entitlement->wallet : null;
+ }
+}
diff --git a/src/app/Handlers/Group.php b/src/app/Handlers/Group.php
new file mode 100644
--- /dev/null
+++ b/src/app/Handlers/Group.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace App\Handlers;
+
+class Group extends \App\Handlers\Base
+{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
+ public static function entitleableClass(): string
+ {
+ return \App\Group::class;
+ }
+}
diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php
--- a/src/app/Http/Controllers/API/SignupController.php
+++ b/src/app/Http/Controllers/API/SignupController.php
@@ -371,7 +371,7 @@
// Check if user with specified login already exists
$email = $login . '@' . $domain;
- if (User::emailExists($email) || User::aliasExists($email)) {
+ if (User::emailExists($email) || User::aliasExists($email) || \App\Group::emailExists($email)) {
return ['login' => \trans('validation.loginexists')];
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Domain;
+use App\Group;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
use App\Sku;
@@ -30,10 +31,10 @@
];
/**
- * On user create it is filled with a user object to force-delete
+ * On user create it is filled with a user or group object to force-delete
* before the creation of a new user record is possible.
*
- * @var \App\User|null
+ * @var \App\User|\App\Group|null
*/
protected $deleteBeforeCreate;
@@ -673,10 +674,10 @@
/**
* Email address validation for use as a user mailbox (login).
*
- * @param string $email Email address
- * @param \App\User $user The account owner
- * @param ?\App\User $deleted Filled with an instance of a deleted user with
- * the specified email address, if exists
+ * @param string $email Email address
+ * @param \App\User $user The account owner
+ * @param null|\App\User|\App\Group $deleted Filled with an instance of a deleted user or group
+ * with the specified email address, if exists
*
* @return ?string Error message on validation error
*/
@@ -734,6 +735,17 @@
return \trans('validation.entryexists', ['attribute' => 'email']);
}
+ // Check if a group with specified address already exists
+ if ($existing_group = Group::emailExists($email, true)) {
+ // If this is a deleted group in the same custom domain
+ // we'll force delete it before
+ if (!$domain->isPublic() && $existing_group->trashed()) {
+ $deleted = $existing_group;
+ } else {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+ }
+
return null;
}
@@ -798,6 +810,11 @@
}
}
+ // Check if a group with specified address already exists
+ if (Group::emailExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'alias']);
+ }
+
return null;
}
}
diff --git a/src/app/Jobs/DomainJob.php b/src/app/Jobs/DomainJob.php
--- a/src/app/Jobs/DomainJob.php
+++ b/src/app/Jobs/DomainJob.php
@@ -31,7 +31,7 @@
/**
* Create a new job instance.
*
- * @param int $domainId The ID for the user to create.
+ * @param int $domainId The ID for the domain to create.
*
* @return void
*/
diff --git a/src/app/Jobs/Group/CreateJob.php b/src/app/Jobs/Group/CreateJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/Group/CreateJob.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Jobs\Group;
+
+use App\Jobs\GroupJob;
+
+class CreateJob extends GroupJob
+{
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $group = $this->getGroup();
+
+ if (!$group->isLdapReady()) {
+ \App\Backends\LDAP::createGroup($group);
+
+ $group->status |= \App\Group::STATUS_LDAP_READY;
+ $group->save();
+ }
+ }
+}
diff --git a/src/app/Jobs/Group/DeleteJob.php b/src/app/Jobs/Group/DeleteJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/Group/DeleteJob.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Jobs\Group;
+
+use App\Jobs\GroupJob;
+
+class DeleteJob extends GroupJob
+{
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $group = $this->getGroup();
+
+ // sanity checks
+ if ($group->isDeleted()) {
+ $this->fail(new \Exception("Group {$this->groupId} is already marked as deleted."));
+ return;
+ }
+
+ \App\Backends\LDAP::deleteGroup($group);
+
+ $group->status |= \App\Group::STATUS_DELETED;
+
+ if ($group->isLdapReady()) {
+ $group->status ^= \App\Group::STATUS_LDAP_READY;
+ }
+
+ $group->save();
+ }
+}
diff --git a/src/app/Jobs/Group/UpdateJob.php b/src/app/Jobs/Group/UpdateJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/Group/UpdateJob.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace App\Jobs\Group;
+
+use App\Jobs\GroupJob;
+
+class UpdateJob extends GroupJob
+{
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $group = $this->getGroup();
+
+ if (!$group->isLdapReady()) {
+ $this->delete();
+ return;
+ }
+
+ \App\Backends\LDAP::updateGroup($group);
+ }
+}
diff --git a/src/app/Jobs/GroupJob.php b/src/app/Jobs/GroupJob.php
new file mode 100644
--- /dev/null
+++ b/src/app/Jobs/GroupJob.php
@@ -0,0 +1,66 @@
+<?php
+
+namespace App\Jobs;
+
+/**
+ * The abstract \App\Jobs\GroupJob implements the logic needed for all dispatchable Jobs related to
+ * \App\Group objects.
+ *
+ * ```php
+ * $job = new \App\Jobs\Group\CreateJob($groupId);
+ * $job->handle();
+ * ```
+ */
+abstract class GroupJob extends CommonJob
+{
+ /**
+ * The ID for the \App\Group. This is the shortest globally unique identifier and saves Redis space
+ * compared to a serialized version of the complete \App\Group object.
+ *
+ * @var int
+ */
+ protected $groupId;
+
+ /**
+ * The \App\Group email property, for legibility in the queue management.
+ *
+ * @var string
+ */
+ protected $groupEmail;
+
+ /**
+ * Create a new job instance.
+ *
+ * @param int $groupId The ID for the group to create.
+ *
+ * @return void
+ */
+ public function __construct(int $groupId)
+ {
+ $this->groupId = $groupId;
+
+ $group = $this->getGroup();
+
+ if ($group) {
+ $this->groupEmail = $group->email;
+ }
+ }
+
+ /**
+ * Get the \App\Group entry associated with this job.
+ *
+ * @return \App\Group|null
+ *
+ * @throws \Exception
+ */
+ protected function getGroup()
+ {
+ $group = \App\Group::withTrashed()->find($this->groupId);
+
+ if (!$group) {
+ $this->fail(new \Exception("Group {$this->groupId} could not be found in the database."));
+ }
+
+ return $group;
+ }
+}
diff --git a/src/app/Observers/GroupObserver.php b/src/app/Observers/GroupObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/GroupObserver.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace App\Observers;
+
+use App\Group;
+use Illuminate\Support\Facades\DB;
+
+class GroupObserver
+{
+ /**
+ * Handle the group "created" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function creating(Group $group): void
+ {
+ while (true) {
+ $allegedly_unique = \App\Utils::uuidInt();
+ if (!Group::find($allegedly_unique)) {
+ $group->{$group->getKeyName()} = $allegedly_unique;
+ break;
+ }
+ }
+
+ $group->status |= Group::STATUS_NEW;
+ }
+
+ /**
+ * Handle the group "created" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function created(Group $group)
+ {
+ \App\Jobs\Group\CreateJob::dispatch($group->id);
+ }
+
+ /**
+ * Handle the group "deleting" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function deleting(Group $group)
+ {
+ // Entitlements do not have referential integrity on the entitled object, so this is our
+ // way of doing an onDelete('cascade') without the foreign key.
+ \App\Entitlement::where('entitleable_id', $group->id)
+ ->where('entitleable_type', Group::class)
+ ->delete();
+ }
+
+ /**
+ * Handle the group "deleted" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function deleted(Group $group)
+ {
+ \App\Jobs\Group\DeleteJob::dispatch($group->id);
+ }
+
+ /**
+ * Handle the group "updated" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function updated(Group $group)
+ {
+ \App\Jobs\Group\UpdateJob::dispatch($group->id);
+ }
+
+ /**
+ * Handle the group "restored" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function restored(Group $group)
+ {
+ //
+ }
+
+ /**
+ * Handle the group "force deleted" event.
+ *
+ * @param \App\Group $group The group
+ *
+ * @return void
+ */
+ public function forceDeleted(Group $group)
+ {
+ //
+ }
+}
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -29,6 +29,7 @@
\App\Discount::observe(\App\Observers\DiscountObserver::class);
\App\Domain::observe(\App\Observers\DomainObserver::class);
\App\Entitlement::observe(\App\Observers\EntitlementObserver::class);
+ \App\Group::observe(\App\Observers\GroupObserver::class);
\App\Package::observe(\App\Observers\PackageObserver::class);
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\Plan::observe(\App\Observers\PlanObserver::class);
diff --git a/src/database/migrations/2020_12_28_140000_create_groups_table.php b/src/database/migrations/2020_12_28_140000_create_groups_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2020_12_28_140000_create_groups_table.php
@@ -0,0 +1,55 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+// phpcs:ignore
+class CreateGroupsTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'groups',
+ function (Blueprint $table) {
+ $table->bigInteger('id');
+ $table->string('email')->unique();
+ $table->text('members')->nullable();
+ $table->smallInteger('status');
+
+ $table->timestamps();
+ $table->softDeletes();
+
+ $table->primary('id');
+ }
+ );
+
+ if (!\App\Sku::where('title', 'group')->first()) {
+ \App\Sku::create([
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ]);
+ }
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('groups');
+ }
+}
diff --git a/src/database/seeds/local/SkuSeeder.php b/src/database/seeds/local/SkuSeeder.php
--- a/src/database/seeds/local/SkuSeeder.php
+++ b/src/database/seeds/local/SkuSeeder.php
@@ -183,5 +183,21 @@
]
);
}
+
+ // Check existence because migration might have added this already
+ if (!\App\Sku::where('title', 'group')->first()) {
+ Sku::create(
+ [
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ]
+ );
+ }
}
}
diff --git a/src/database/seeds/production/SkuSeeder.php b/src/database/seeds/production/SkuSeeder.php
--- a/src/database/seeds/production/SkuSeeder.php
+++ b/src/database/seeds/production/SkuSeeder.php
@@ -183,5 +183,21 @@
]
);
}
+
+ // Check existence because migration might have added this already
+ if (!\App\Sku::where('title', 'group')->first()) {
+ Sku::create(
+ [
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ]
+ );
+ }
}
}
diff --git a/src/tests/Feature/Backends/LDAPTest.php b/src/tests/Feature/Backends/LDAPTest.php
--- a/src/tests/Feature/Backends/LDAPTest.php
+++ b/src/tests/Feature/Backends/LDAPTest.php
@@ -109,6 +109,16 @@
$this->assertSame(null, LDAP::getDomain($domain->namespace));
}
+ /**
+ * Test creating/updating/deleting a group record
+ *
+ * @group ldap
+ */
+ public function testGroup(): void
+ {
+ $this->markTestIncomplete();
+ }
+
/**
* Test creating/editing/deleting a user record
*
diff --git a/src/tests/Feature/Console/Group/AddMemberTest.php b/src/tests/Feature/Console/Group/AddMemberTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/AddMemberTest.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class AddMemberTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+
+ // Non-existing group
+ $code = \Artisan::call("group:add-member test@group.com member@group.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Group test@group.com does not exist.", $output);
+
+ $group = Group::create(['email' => 'group-test@kolabnow.com']);
+
+ // Existing group, invalid member
+ $code = \Artisan::call("group:add-member {$group->email} member");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("member: The specified email address is invalid.", $output);
+
+ // Existing group
+ $code = \Artisan::call("group:add-member {$group->email} member@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertSame(['member@gmail.com'], $group->refresh()->members);
+
+ // Existing group
+ $code = \Artisan::call("group:add-member {$group->email} member2@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertSame(['member@gmail.com', 'member2@gmail.com'], $group->refresh()->members);
+
+ // Add a member that already exists
+ $code = \Artisan::call("group:add-member {$group->email} member@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("member@gmail.com: Already exists in the group.", $output);
+ $this->assertSame(['member@gmail.com', 'member2@gmail.com'], $group->refresh()->members);
+ }
+}
diff --git a/src/tests/Feature/Console/Group/CreateTest.php b/src/tests/Feature/Console/Group/CreateTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/CreateTest.php
@@ -0,0 +1,112 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class CreateTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestGroup('group-testm@kolabnow.com');
+ $this->deleteTestUser('group-owner@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestGroup('group-testm@kolabnow.com');
+ $this->deleteTestUser('group-owner@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+
+ // Missing group owner argument
+ $code = \Artisan::call("group:create group-test@kolabnow.com");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("The --user option is required.", $output);
+
+ // Invalid group owner argument
+ $code = \Artisan::call("group:create group-test@kolabnow.com --user=nonexisting@nonexisting.org");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("User nonexisting@nonexisting.org does not exist.", $output);
+
+ $user = $this->getTestUser('group-owner@kolabnow.com');
+
+ // Domain not available
+ $code = \Artisan::call("group:create testgroup@kolab.org --user={$user->id}");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("testgroup@kolab.org: Domain not available.", $output);
+
+ // Existing email
+ $code = \Artisan::call("group:create jack@kolab.org --user=john@kolab.org");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("jack@kolab.org: The specified email is not available.", $output);
+
+ // Existing email (of a user alias)
+ $code = \Artisan::call("group:create jack.daniels@kolab.org --user=john@kolab.org");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("jack.daniels@kolab.org: The specified email is not available.", $output);
+
+ // Create a group without members
+ $code = \Artisan::call("group:create group-test@kolabnow.com --user={$user->email}");
+ $output = trim(\Artisan::output());
+ $group = Group::where('email', 'group-test@kolabnow.com')->first();
+
+ $this->assertSame(0, $code);
+ $this->assertEquals($group->id, $output);
+ $this->assertSame('group-test@kolabnow.com', $group->email);
+ $this->assertSame([], $group->members);
+ $this->assertSame($user->wallets->first()->id, $group->entitlement->wallet_id);
+
+ // Existing email (of a group)
+ $code = \Artisan::call("group:create group-test@kolabnow.com --user={$user->email}");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("group-test@kolabnow.com: The specified email is not available.", $output);
+
+ // Invalid member
+ $code = \Artisan::call("group:create group-testm@kolabnow.com --user={$user->email} --member=invalid");
+ $output = trim(\Artisan::output());
+ $this->assertSame(1, $code);
+ $this->assertSame("invalid: The specified email address is invalid.", $output);
+
+ // Valid members
+ $code = \Artisan::call(
+ "group:create group-testm@kolabnow.com --user={$user->email}"
+ . " --member=member1@kolabnow.com --member=member2@gmail.com"
+ );
+ $output = trim(\Artisan::output());
+ $group = Group::where('email', 'group-testm@kolabnow.com')->first();
+ $this->assertSame(0, $code);
+ $this->assertEquals($group->id, $output);
+ $this->assertSame('group-testm@kolabnow.com', $group->email);
+ $this->assertSame(['member1@kolabnow.com', 'member2@gmail.com'], $group->members);
+ }
+}
diff --git a/src/tests/Feature/Console/Group/DeleteTest.php b/src/tests/Feature/Console/Group/DeleteTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/DeleteTest.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class DeleteTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestUser('group-owner@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestUser('group-owner@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+
+ // Non-existing group
+ $code = \Artisan::call("group:delete test@group.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Group test@group.com does not exist.", $output);
+
+ $user = $this->getTestUser('group-owner@kolabnow.com');
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ // Existing group
+ $code = \Artisan::call("group:delete {$group->email}");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertTrue($group->refresh()->trashed());
+ }
+}
diff --git a/src/tests/Feature/Console/Group/InfoTest.php b/src/tests/Feature/Console/Group/InfoTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/InfoTest.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Tests\TestCase;
+
+class InfoTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ // Non-existing group
+ $this->artisan("group:info unknown@unknown.org")
+ ->assertExitCode(1)
+ ->expectsOutput("Group unknown@unknown.org does not exist.");
+
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+
+ // A group with no members
+ $this->artisan("group:info {$group->email}")
+ ->assertExitCode(0);
+
+ // TODO: Test output
+ // $expected = "Id: {$group->id}\nEmail: {$group->email}\nStatus: {$group->status}\n";
+ }
+}
diff --git a/src/tests/Feature/Console/Group/RemoveMemberTest.php b/src/tests/Feature/Console/Group/RemoveMemberTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Group/RemoveMemberTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Tests\Feature\Console\Group;
+
+use App\Group;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class RemoveMemberTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command runs
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Warning: We're not using artisan() here, as this will not
+ // allow us to test "empty output" cases
+
+ // Non-existing group
+ $code = \Artisan::call("group:remove-member test@group.com member@group.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Group test@group.com does not exist.", $output);
+
+ $group = Group::create([
+ 'email' => 'group-test@kolabnow.com',
+ 'members' => ['member1@gmail.com', 'member2@gmail.com'],
+ ]);
+
+ // Existing group, non-existing member
+ $code = \Artisan::call("group:remove-member {$group->email} nonexisting@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Member nonexisting@gmail.com not found in the group.", $output);
+
+ // Existing group, existing member
+ $code = \Artisan::call("group:remove-member {$group->email} member1@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertSame(['member2@gmail.com'], $group->refresh()->members);
+
+ // Existing group, the last existing member
+ $code = \Artisan::call("group:remove-member {$group->email} member2@gmail.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame('', $output);
+ $this->assertSame([], $group->refresh()->members);
+ }
+}
diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php
--- a/src/tests/Feature/Controller/SignupTest.php
+++ b/src/tests/Feature/Controller/SignupTest.php
@@ -31,6 +31,8 @@
$this->deleteTestDomain('external.com');
$this->deleteTestDomain('signup-domain.com');
+
+ $this->deleteTestGroup('group-test@kolabnow.com');
}
/**
@@ -45,6 +47,8 @@
$this->deleteTestDomain('external.com');
$this->deleteTestDomain('signup-domain.com');
+ $this->deleteTestGroup('group-test@kolabnow.com');
+
parent::tearDown();
}
@@ -686,4 +690,21 @@
$this->assertSame($expected_result, $result);
}
+
+ /**
+ * Signup login/domain validation, more cases
+ *
+ * Note: Technically these include unit tests, but let's keep it here for now.
+ */
+ public function testValidateLoginMore(): void
+ {
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+ $login = 'group-test';
+ $domain = 'kolabnow.com';
+ $external = false;
+
+ $result = $this->invokeMethod(new SignupController(), 'validateLogin', [$login, $domain, $external]);
+
+ $this->assertSame(['login' => 'The specified login is not available.'], $result);
+ }
}
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -32,6 +32,8 @@
$this->deleteTestUser('deleted@kolab.org');
$this->deleteTestUser('deleted@kolabnow.com');
$this->deleteTestDomain('userscontroller.com');
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestGroup('group-test@kolab.org');
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
@@ -56,6 +58,8 @@
$this->deleteTestUser('deleted@kolab.org');
$this->deleteTestUser('deleted@kolabnow.com');
$this->deleteTestDomain('userscontroller.com');
+ $this->deleteTestGroup('group-test@kolabnow.com');
+ $this->deleteTestGroup('group-test@kolab.org');
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
@@ -1125,6 +1129,45 @@
$this->assertSame(null, $deleted);
}
+ /**
+ * User email validation - tests for an address being a group email address
+ *
+ * Note: Technically these include unit tests, but let's keep it here for now.
+ * FIXME: Shall we do a http request for each case?
+ */
+ public function testValidateEmailGroup(): void
+ {
+ Queue::fake();
+
+ $john = $this->getTestUser('john@kolab.org');
+ $pub_group = $this->getTestGroup('group-test@kolabnow.com');
+ $priv_group = $this->getTestGroup('group-test@kolab.org');
+
+ // A group in a public domain, existing
+ $result = UsersController::validateEmail($pub_group->email, $john, $deleted);
+ $this->assertSame('The specified email is not available.', $result);
+ $this->assertNull($deleted);
+
+ $pub_group->delete();
+
+ // A group in a public domain, deleted
+ $result = UsersController::validateEmail($pub_group->email, $john, $deleted);
+ $this->assertSame('The specified email is not available.', $result);
+ $this->assertNull($deleted);
+
+ // A group in a private domain, existing
+ $result = UsersController::validateEmail($priv_group->email, $john, $deleted);
+ $this->assertSame('The specified email is not available.', $result);
+ $this->assertNull($deleted);
+
+ $priv_group->delete();
+
+ // A group in a private domain, deleted
+ $result = UsersController::validateEmail($priv_group->email, $john, $deleted);
+ $this->assertSame(null, $result);
+ $this->assertSame($priv_group->id, $deleted->id);
+ }
+
/**
* List of alias validation cases for testValidateAlias()
*
@@ -1204,6 +1247,7 @@
$deleted_pub = $this->getTestUser('deleted@kolabnow.com');
$deleted_pub->setAliases(['deleted-alias@kolabnow.com']);
$deleted_pub->delete();
+ $group = $this->getTestGroup('group-test@kolabnow.com');
// An alias that was a user email before is allowed, but only for custom domains
$result = UsersController::validateAlias('deleted@kolab.org', $john);
@@ -1217,5 +1261,9 @@
$result = UsersController::validateAlias('deleted-alias@kolabnow.com', $john);
$this->assertSame('The specified alias is not available.', $result);
+
+ // A grpoup with the same email address exists
+ $result = UsersController::validateAlias($group->email, $john);
+ $this->assertSame('The specified alias is not available.', $result);
}
}
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -3,6 +3,7 @@
namespace Tests;
use App\Domain;
+use App\Group;
use App\Transaction;
use App\User;
use Carbon\Carbon;
@@ -149,6 +150,22 @@
$domain->forceDelete();
}
+ protected function deleteTestGroup($email)
+ {
+ Queue::fake();
+
+ $group = Group::withTrashed()->where('email', $email)->first();
+
+ if (!$group) {
+ return;
+ }
+
+ $job = new \App\Jobs\Group\DeleteJob($group->id);
+ $job->handle();
+
+ $group->forceDelete();
+ }
+
protected function deleteTestUser($email)
{
Queue::fake();
@@ -176,6 +193,17 @@
return Domain::firstOrCreate(['namespace' => $name], $attrib);
}
+ /**
+ * Get Group object by email, create it if needed.
+ * Skip LDAP jobs.
+ */
+ protected function getTestGroup($email, $attrib = [])
+ {
+ // Disable jobs (i.e. skip LDAP oprations)
+ Queue::fake();
+ return Group::firstOrCreate(['email' => $email], $attrib);
+ }
+
/**
* Get User object by email, create it if needed.
* Skip LDAP jobs.

File Metadata

Mime Type
text/plain
Expires
Sun, Mar 29, 10:54 PM (6 d, 38 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18778365
Default Alt Text
D2020.1774824860.diff (51 KB)

Event Timeline