Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117392409
D2020.1774824860.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
51 KB
Referenced Files
None
Subscribers
None
D2020.1774824860.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D2020: Mail enabled distribution groups
Attached
Detach File
Event Timeline