diff options
author | Daniel Kesselberg <mail@danielkesselberg.de> | 2021-06-16 15:39:03 +0300 |
---|---|---|
committer | Daniel Kesselberg <mail@danielkesselberg.de> | 2021-07-06 23:04:31 +0300 |
commit | 8fb14b5ffa3450b2c2201ff9bb42c589e4a4afb7 (patch) | |
tree | a5b84701a79eca0784c96a87b37bb20127c4ba86 | |
parent | 70ef1726ad78b25db571e4b8096eaaf2fa2d84c2 (diff) |
Add provisioning for aliases
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
-rw-r--r-- | lib/Controller/AliasesController.php | 9 | ||||
-rw-r--r-- | lib/Controller/SettingsController.php | 9 | ||||
-rw-r--r-- | lib/Db/Alias.php | 12 | ||||
-rw-r--r-- | lib/Db/AliasMapper.php | 45 | ||||
-rw-r--r-- | lib/Db/Provisioning.php | 14 | ||||
-rw-r--r-- | lib/Db/ProvisioningMapper.php | 23 | ||||
-rw-r--r-- | lib/Migration/Version1101Date20210616141806.php | 34 | ||||
-rw-r--r-- | lib/Service/AliasesService.php | 44 | ||||
-rw-r--r-- | lib/Service/Provisioning/Manager.php | 167 | ||||
-rw-r--r-- | lib/Settings/AdminSettings.php | 9 | ||||
-rw-r--r-- | src/components/AccountSettings.vue | 21 | ||||
-rw-r--r-- | src/components/AliasForm.vue | 158 | ||||
-rw-r--r-- | src/components/AliasSettings.vue | 123 | ||||
-rw-r--r-- | src/components/settings/ProvisioningSettings.vue | 41 | ||||
-rw-r--r-- | src/service/AliasService.js | 63 | ||||
-rw-r--r-- | src/store/actions.js | 35 | ||||
-rw-r--r-- | src/store/mutations.js | 13 | ||||
-rw-r--r-- | tests/Unit/Controller/AliasesControllerTest.php | 77 | ||||
-rw-r--r-- | tests/Unit/Service/AliasesServiceTest.php | 117 |
19 files changed, 806 insertions, 208 deletions
diff --git a/lib/Controller/AliasesController.php b/lib/Controller/AliasesController.php index 8c1b49ddc..add3efb5f 100644 --- a/lib/Controller/AliasesController.php +++ b/lib/Controller/AliasesController.php @@ -72,8 +72,8 @@ class AliasesController extends Controller { * @NoAdminRequired * @TrapError */ - public function update() { - throw new NotImplemented(); + public function update(int $id, string $alias, string $aliasName): JSONResponse { + return new JSONResponse($this->aliasService->update($this->currentUserId, $id, $alias, $aliasName)); } /** @@ -84,7 +84,7 @@ class AliasesController extends Controller { * @return JSONResponse */ public function destroy(int $id): JSONResponse { - return new JSONResponse($this->aliasService->delete($id, $this->currentUserId)); + return new JSONResponse($this->aliasService->delete($this->currentUserId, $id)); } /** @@ -116,7 +116,6 @@ class AliasesController extends Controller { * @throws DoesNotExistException */ public function updateSignature(int $id, string $signature = null): JSONResponse { - $this->aliasService->updateSignature($this->currentUserId, $id, $signature); - return new JSONResponse(); + return new JSONResponse($this->aliasService->updateSignature($this->currentUserId, $id, $signature)); } } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 2081ea5bb..dd1a19dc4 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -60,6 +60,8 @@ class SettingsController extends Controller { $this->provisioningManager->newProvisioning($data); } catch (ValidationException $e) { return HttpJsonResponse::fail([$e->getFields()]); + } catch (\Exception $e) { + return HttpJsonResponse::fail([$e->getMessage()]); } return new JSONResponse([]); @@ -67,12 +69,11 @@ class SettingsController extends Controller { public function updateProvisioning(int $id, array $data): JSONResponse { try { - $this->provisioningManager->updateProvisioning(array_merge( - $data, - ['id' => $id] - )); + $this->provisioningManager->updateProvisioning(array_merge($data, ['id' => $id])); } catch (ValidationException $e) { return HttpJsonResponse::fail([$e->getFields()]); + } catch (\Exception $e) { + return HttpJsonResponse::fail([$e->getMessage()]); } return new JSONResponse([]); diff --git a/lib/Db/Alias.php b/lib/Db/Alias.php index e1727a83c..812682678 100644 --- a/lib/Db/Alias.php +++ b/lib/Db/Alias.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace OCA\Mail\Db; use JsonSerializable; - use OCP\AppFramework\Db\Entity; /** @@ -36,6 +35,8 @@ use OCP\AppFramework\Db\Entity; * @method string getAlias() * @method void setSignature(string|null $signature) * @method string|null getSignature() + * @method void setProvisioningId(int $provisioningId) + * @method int|null getProvisioningId() */ class Alias extends Entity implements JsonSerializable { @@ -51,10 +52,18 @@ class Alias extends Entity implements JsonSerializable { /** @var string|null */ protected $signature; + /** @var int|null */ + protected $provisioningId; + public function __construct() { $this->addType('accountId', 'int'); $this->addType('name', 'string'); $this->addType('alias', 'string'); + $this->addType('provisioningId', 'int'); + } + + public function isProvisioned(): bool { + return $this->getProvisioningId() !== null; } public function jsonSerialize(): array { @@ -63,6 +72,7 @@ class Alias extends Entity implements JsonSerializable { 'name' => $this->getName(), 'alias' => $this->getAlias(), 'signature' => $this->getSignature(), + 'provisioned' => $this->isProvisioned(), ]; } } diff --git a/lib/Db/AliasMapper.php b/lib/Db/AliasMapper.php index bf2e17b5d..ad95fc5b6 100644 --- a/lib/Db/AliasMapper.php +++ b/lib/Db/AliasMapper.php @@ -41,7 +41,7 @@ class AliasMapper extends QBMapper { */ public function find(int $aliasId, string $currentUserId): Alias { $qb = $this->db->getQueryBuilder(); - $qb->select('aliases.*') + $qb->select('aliases.*', 'accounts.provisioning_id') ->from($this->getTableName(), 'aliases') ->join('aliases', 'mail_accounts', 'accounts', $qb->expr()->eq('aliases.account_id', 'accounts.id')) ->where( @@ -55,6 +55,24 @@ class AliasMapper extends QBMapper { } /** + * @throws DoesNotExistException + */ + public function findByAlias(string $alias, string $currentUserId): Alias { + $qb = $this->db->getQueryBuilder(); + $qb->select('aliases.*', 'accounts.provisioning_id') + ->from($this->getTableName(), 'aliases') + ->join('aliases', 'mail_accounts', 'accounts', $qb->expr()->eq('aliases.account_id', 'accounts.id')) + ->where( + $qb->expr()->andX( + $qb->expr()->eq('accounts.user_id', $qb->createNamedParameter($currentUserId)), + $qb->expr()->eq('aliases.alias', $qb->createNamedParameter($alias)) + ) + ); + + return $this->findEntity($qb); + } + + /** * @param int $accountId * @param string $currentUserId * @@ -62,7 +80,7 @@ class AliasMapper extends QBMapper { */ public function findAll(int $accountId, string $currentUserId): array { $qb = $this->db->getQueryBuilder(); - $qb->select('aliases.*') + $qb->select('aliases.*', 'accounts.provisioning_id') ->from($this->getTableName(), 'aliases') ->join('aliases', 'mail_accounts', 'accounts', $qb->expr()->eq('aliases.account_id', 'accounts.id')) ->where( @@ -89,6 +107,29 @@ class AliasMapper extends QBMapper { $query->execute(); } + /** + * Delete all provisioned aliases for the given uid + * + * Exception for Nextcloud 20: \Doctrine\DBAL\DBALException + * Exception for Nextcloud 21 and newer: \OCP\DB\Exception + * + * @TODO: Change throws to \OCP\DB\Exception once Mail does not support Nextcloud 20. + * + * @throws \Exception + */ + public function deleteProvisionedAliasesByUid(string $uid): void { + $qb = $this->db->getQueryBuilder(); + + $qb->delete($this->getTableName(), 'aliases') + ->join('aliases', 'mail_accounts', 'accounts', 'accounts.id = aliases.account_id') + ->where( + $qb->expr()->eq('accounts.user_id', $qb->createNamedParameter($uid)), + $qb->expr()->isNotNull('provisioning_id') + ); + + $qb->execute(); + } + public function deleteOrphans(): void { $qb1 = $this->db->getQueryBuilder(); $idsQuery = $qb1->select('a.id') diff --git a/lib/Db/Provisioning.php b/lib/Db/Provisioning.php index 97d3c7a81..b29afa91c 100644 --- a/lib/Db/Provisioning.php +++ b/lib/Db/Provisioning.php @@ -60,6 +60,12 @@ use OCP\IUser; * @method void setSieveSslMode(?string $sieveSslMode) * @method string|null getSieveUser() * @method void setSieveUser(?string $sieveUser) + * @method array getAliases() + * @method void setAliases(array $aliases) + * @method bool getLdapAliasesProvisioning() + * @method void setLdapAliasesProvisioning(bool $ldapAliasesProvisioning) + * @method string|null getLdapAliasesAttribute() + * @method void setLdapAliasesAttribute(?string $ldapAliasesAttribute) */ class Provisioning extends Entity implements JsonSerializable { public const WILDCARD = '*'; @@ -79,13 +85,18 @@ class Provisioning extends Entity implements JsonSerializable { protected $sieveHost; protected $sievePort; protected $sieveSslMode; + protected $aliases = []; + protected $ldapAliasesProvisioning; + protected $ldapAliasesAttribute; public function __construct() { $this->addType('imapPort', 'integer'); $this->addType('smtpPort', 'integer'); $this->addType('sieveEnabled', 'boolean'); $this->addType('sievePort', 'integer'); + $this->addType('ldapAliasesProvisioning', 'boolean'); } + /** * @return array */ @@ -107,6 +118,9 @@ class Provisioning extends Entity implements JsonSerializable { 'sieveHost' => $this->getSieveHost(), 'sievePort' => $this->getSievePort(), 'sieveSslMode' => $this->getSieveSslMode(), + 'aliases' => $this->getAliases(), + 'ldapAliasesProvisioning' => $this->getLdapAliasesProvisioning(), + 'ldapAliasesAttribute' => $this->getLdapAliasesAttribute(), ]; } diff --git a/lib/Db/ProvisioningMapper.php b/lib/Db/ProvisioningMapper.php index 09eec6281..0daca261f 100644 --- a/lib/Db/ProvisioningMapper.php +++ b/lib/Db/ProvisioningMapper.php @@ -51,11 +51,11 @@ class ProvisioningMapper extends QBMapper { * * @return Provisioning[] */ - public function getAll() : array { + public function getAll(): array { $qb = $this->db->getQueryBuilder(); $qb = $qb->select('*') - ->from($this->getTableName()) - ->orderBy('provisioning_domain', 'desc'); + ->from($this->getTableName()) + ->orderBy('provisioning_domain', 'desc'); try { return $this->findEntities($qb); } catch (DoesNotExistException $e) { @@ -69,7 +69,7 @@ class ProvisioningMapper extends QBMapper { * @return Provisioning * @throws ValidationException */ - public function validate(array $data) : Provisioning { + public function validate(array $data): Provisioning { $exception = new ValidationException(); if (!isset($data['provisioningDomain']) || $data['provisioningDomain'] === '') { @@ -103,11 +103,17 @@ class ProvisioningMapper extends QBMapper { $exception->setField('smtpSslMode', false); } + $ldapAliasesProvisioning = (bool)($data['ldapAliasesProvisioning'] ?? false); + $ldapAliasesAttribute = $data['ldapAliasesAttribute'] ?? ''; + + if ($ldapAliasesProvisioning && empty($ldapAliasesAttribute)) { + $exception->setField('ldapAliasesAttribute', false); + } + if (!empty($exception->getFields())) { throw $exception; } - $provisioning = new Provisioning(); $provisioning->setId($data['id'] ?? null); $provisioning->setProvisioningDomain($data['provisioningDomain']); @@ -127,14 +133,17 @@ class ProvisioningMapper extends QBMapper { $provisioning->setSievePort($data['sievePort'] ?? null); $provisioning->setSieveSslMode($data['sieveSslMode'] ?? ''); + $provisioning->setLdapAliasesProvisioning($ldapAliasesProvisioning); + $provisioning->setLdapAliasesAttribute($ldapAliasesAttribute); + return $provisioning; } public function get(int $id): ?Provisioning { $qb = $this->db->getQueryBuilder(); $qb = $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id), IQueryBuilder::PARAM_INT)); + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id), IQueryBuilder::PARAM_INT)); try { return $this->findEntity($qb); } catch (DoesNotExistException $e) { diff --git a/lib/Migration/Version1101Date20210616141806.php b/lib/Migration/Version1101Date20210616141806.php new file mode 100644 index 000000000..0687b9091 --- /dev/null +++ b/lib/Migration/Version1101Date20210616141806.php @@ -0,0 +1,34 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use Doctrine\DBAL\Schema\SchemaException; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1101Date20210616141806 extends SimpleMigrationStep { + /** + * @throws SchemaException + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $provisioningTable = $schema->getTable('mail_provisionings'); + $provisioningTable->addColumn('ldap_aliases_provisioning', 'boolean', [ + 'notnull' => false, + 'default' => false + ]); + $provisioningTable->addColumn('ldap_aliases_attribute', 'string', [ + 'notnull' => false, + 'length' => 255, + 'default' => '', + ]); + + return $schema; + } +} diff --git a/lib/Service/AliasesService.php b/lib/Service/AliasesService.php index db21e1388..7d308b97c 100644 --- a/lib/Service/AliasesService.php +++ b/lib/Service/AliasesService.php @@ -26,6 +26,7 @@ namespace OCA\Mail\Service; use OCA\Mail\Db\Alias; use OCA\Mail\Db\AliasMapper; use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Exception\ClientException; use OCP\AppFramework\Db\DoesNotExistException; class AliasesService { @@ -81,15 +82,17 @@ class AliasesService { } /** - * @param int $aliasId - * @param String $currentUserId - * @return Alias + * @throws ClientException * @throws DoesNotExistException */ - public function delete(int $aliasId, string $currentUserId): Alias { - $alias = $this->aliasMapper->find($aliasId, $currentUserId); - $this->aliasMapper->delete($alias); - return $alias; + public function delete(string $userId, int $aliasId): Alias { + $entity = $this->aliasMapper->find($aliasId, $userId); + + if ($entity->isProvisioned()) { + throw new ClientException('Deleting a provisioned alias is not allowed.'); + } + + return $this->aliasMapper->delete($entity); } /** @@ -105,16 +108,29 @@ class AliasesService { } /** + * Update alias and name + * + * @throws DoesNotExistException + */ + public function update(string $userId, int $aliasId, string $alias, string $aliasName): Alias { + $entity = $this->aliasMapper->find($aliasId, $userId); + + if (!$entity->isProvisioned()) { + $entity->setAlias($alias); + } + $entity->setName($aliasName); + + return $this->aliasMapper->update($entity); + } + + /** * Update signature for alias * - * @param string $userId - * @param int $aliasId - * @param string|null $signature * @throws DoesNotExistException */ - public function updateSignature(string $userId, int $aliasId, string $signature = null): void { - $alias = $this->find($aliasId, $userId); - $alias->setSignature($signature); - $this->aliasMapper->update($alias); + public function updateSignature(string $userId, int $aliasId, string $signature = null): Alias { + $entity = $this->find($aliasId, $userId); + $entity->setSignature($signature); + return $this->aliasMapper->update($entity); } } diff --git a/lib/Service/Provisioning/Manager.php b/lib/Service/Provisioning/Manager.php index 8cc9c2e06..6f06c330e 100644 --- a/lib/Service/Provisioning/Manager.php +++ b/lib/Service/Provisioning/Manager.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace OCA\Mail\Service\Provisioning; use Horde_Mail_Rfc822_Address; +use OCA\Mail\Db\Alias; +use OCA\Mail\Db\AliasMapper; use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailAccountMapper; use OCA\Mail\Db\Provisioning; @@ -33,6 +35,8 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\IUser; use OCP\IUserManager; +use OCP\LDAP\ILDAPProvider; +use OCP\LDAP\ILDAPProviderFactory; use OCP\Security\ICrypto; use Psr\Log\LoggerInterface; @@ -50,6 +54,12 @@ class Manager { /** @var ICrypto */ private $crypto; + /** @var ILDAPProviderFactory */ + private $ldapProviderFactory; + + /** @var AliasMapper */ + private $aliasMapper; + /** @var LoggerInterface */ private $logger; @@ -57,11 +67,15 @@ class Manager { ProvisioningMapper $provisioningMapper, MailAccountMapper $mailAccountMapper, ICrypto $crypto, + ILDAPProviderFactory $ldapProviderFactory, + AliasMapper $aliasMapper, LoggerInterface $logger) { $this->userManager = $userManager; $this->provisioningMapper = $provisioningMapper; $this->mailAccountMapper = $mailAccountMapper; $this->crypto = $crypto; + $this->ldapProviderFactory = $ldapProviderFactory; + $this->aliasMapper = $aliasMapper; $this->logger = $logger; } @@ -85,6 +99,75 @@ class Manager { } /** + * Delete orphaned aliases for the given account. + * + * A alias is orphaned if not listed in newAliases anymore + * (=> the provisioning configuration does contain it anymore) + * + * Exception for Nextcloud 20: \Doctrine\DBAL\DBALException + * Exception for Nextcloud 21 and newer: \OCP\DB\Exception + * + * @TODO: Change throws to \OCP\DB\Exception once Mail requires Nextcloud 21 or above + * + * @throws \Exception + */ + private function deleteOrphanedAliases(string $userId, int $accountId, array $newAliases): void { + $existingAliases = $this->aliasMapper->findAll($accountId, $userId); + foreach ($existingAliases as $existingAlias) { + if (!in_array($existingAlias->getAlias(), $newAliases, true)) { + $this->aliasMapper->delete($existingAlias); + } + } + } + + /** + * Create new aliases for the given account. + * + * Exception for Nextcloud 20: \Doctrine\DBAL\DBALException + * Exception for Nextcloud 21 and newer: \OCP\DB\Exception + * + * @TODO: Change throws to \OCP\DB\Exception once Mail requires Nextcloud 21 or above + * + * @throws \Exception + */ + private function createNewAliases(string $userId, int $accountId, array $newAliases, string $displayName): void { + foreach ($newAliases as $newAlias) { + try { + $this->aliasMapper->findByAlias($newAlias, $userId); + } catch (DoesNotExistException $e) { + $alias = new Alias(); + $alias->setAccountId($accountId); + $alias->setName($displayName); + $alias->setAlias($newAlias); + $this->aliasMapper->insert($alias); + } + } + } + + /** + * @throws \Exception if user id was not found in LDAP + * + * @TODO: Remove psalm-suppress once Mail requires Nextcloud 22 or above + */ + public function ldapAliasesIntegration(Provisioning $provisioning, IUser $user): Provisioning { + if ($user->getBackendClassName() !== 'LDAP' || $provisioning->getLdapAliasesProvisioning() === false || empty($provisioning->getLdapAliasesAttribute())) { + return $provisioning; + } + + /** @psalm-suppress UndefinedInterfaceMethod */ + if ($this->ldapProviderFactory->isAvailable() === false) { + $this->logger->debug('Request to provision mail aliases but ldap not available'); + return $provisioning; + } + + $ldapProvider = $this->ldapProviderFactory->getLDAPProvider(); + /** @psalm-suppress UndefinedInterfaceMethod */ + $provisioning->setAliases($ldapProvider->getMultiValueUserAttribute($user->getUID(), $provisioning->getLdapAliasesAttribute())); + + return $provisioning; + } + + /** * @param Provisioning[] $provisionings */ public function provisionSingleUser(array $provisionings, IUser $user): bool { @@ -96,57 +179,69 @@ class Manager { try { // TODO: match by UID only, catch multiple objects returned below and delete all those accounts - $existing = $this->mailAccountMapper->findProvisionedAccount($user); + $mailAccount = $this->mailAccountMapper->findProvisionedAccount($user); - $this->mailAccountMapper->update( - $this->updateAccount($user, $existing, $provisioning) + $mailAccount = $this->mailAccountMapper->update( + $this->updateAccount($user, $mailAccount, $provisioning) ); - return true; - } catch (DoesNotExistException $e) { + } catch (DoesNotExistException | MultipleObjectsReturnedException $e) { + if ($e instanceof MultipleObjectsReturnedException) { + // This is unlikely to happen but not impossible. + // Let's wipe any existing accounts and start fresh + $this->aliasMapper->deleteProvisionedAliasesByUid($user->getUID()); + $this->mailAccountMapper->deleteProvisionedAccountsByUid($user->getUID()); + } + // Fine, then we create a new one - $new = new MailAccount(); - $new->setUserId($user->getUID()); + $mailAccount = new MailAccount(); + $mailAccount->setUserId($user->getUID()); - $this->mailAccountMapper->insert( - $this->updateAccount($user, $new, $provisioning) + $mailAccount = $this->mailAccountMapper->insert( + $this->updateAccount($user, $mailAccount, $provisioning) ); - return true; - } catch (MultipleObjectsReturnedException $e) { - // This is unlikely to happen but not impossible. - // Let's wipe any existing accounts and start fresh - $this->mailAccountMapper->deleteProvisionedAccountsByUid($user->getUID()); + } - $new = new MailAccount(); - $new->setUserId($user->getUID()); + // @TODO: Remove method_exists once Mail requires Nextcloud 22 or above + if (method_exists(ILDAPProvider::class, 'getMultiValueUserAttribute')) { + try { + $provisioning = $this->ldapAliasesIntegration($provisioning, $user); + } catch (\Throwable $e) { + $this->logger->warning('Request to provision mail aliases failed', ['exception' => $e]); + // return here to avoid provisioning of aliases. + return true; + } - $this->mailAccountMapper->insert( - $this->updateAccount($user, $new, $provisioning) - ); - return true; + try { + $this->deleteOrphanedAliases($user->getUID(), $mailAccount->getId(), $provisioning->getAliases()); + } catch (\Throwable $e) { + $this->logger->warning('Deleting orphaned aliases failed', ['exception' => $e]); + } + + try { + $this->createNewAliases($user->getUID(), $mailAccount->getId(), $provisioning->getAliases(), $user->getDisplayName()); + } catch (\Throwable $e) { + $this->logger->warning('Creating new aliases failed', ['exception' => $e]); + } } - return false; + + return true; } + /** + * @throws ValidationException + * @throws \Exception + */ public function newProvisioning(array $data): void { - try { - $provisioning = $this->provisioningMapper->validate( - $data - ); - } catch (ValidationException $e) { - throw $e; - } + $provisioning = $this->provisioningMapper->validate($data); $this->provisioningMapper->insert($provisioning); } + /** + * @throws ValidationException + * @throws \Exception + */ public function updateProvisioning(array $data): void { - try { - $provisioning = $this->provisioningMapper->validate( - $data - ); - } catch (ValidationException $e) { - throw $e; - } - + $provisioning = $this->provisioningMapper->validate($data); $this->provisioningMapper->update($provisioning); } diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index 2a8d0fd4e..428201244 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -29,6 +29,7 @@ use OCA\Mail\AppInfo\Application; use OCA\Mail\Service\Provisioning\Manager as ProvisioningManager; use OCP\AppFramework\Http\TemplateResponse; use OCP\IInitialStateService; +use OCP\LDAP\ILDAPProvider; use OCP\Settings\ISettings; class AdminSettings implements ISettings { @@ -52,6 +53,14 @@ class AdminSettings implements ISettings { $this->provisioningManager->getConfigs() ); + $this->initialStateService->provideLazyInitialState( + Application::APP_ID, + 'ldap_aliases_integration', + function () { + return method_exists(ILDAPProvider::class, 'getMultiValueUserAttribute'); + } + ); + return new TemplateResponse(Application::APP_ID, 'settings-admin'); } diff --git a/src/components/AccountSettings.vue b/src/components/AccountSettings.vue index c6503a03e..0af5beb70 100644 --- a/src/components/AccountSettings.vue +++ b/src/components/AccountSettings.vue @@ -28,13 +28,15 @@ :show-navigation="true"> <AppSettingsSection :title="t('mail', 'Account settings')"> - <strong>{{ displayName }}</strong> <{{ email }}> - <a - v-if="!account.provisioningId" - class="button icon-rename" - :title="t('mail', 'Change name')" - @click="handleClick" /> - <AliasSettings v-if="!account.provisioningId" :account="account" /> + <div class="alias-item"> + <p><strong>{{ displayName }}</strong> <{{ email }}></p> + <a + v-if="!account.provisioningId" + class="button icon-rename" + :title="t('mail', 'Change name')" + @click="handleClick" /> + </div> + <AliasSettings :account="account" /> </AppSettingsSection> <AppSettingsSection :title="t('mail', 'Signature')"> <p class="settings-hint"> @@ -178,6 +180,11 @@ export default { </script> <style lang="scss" scoped> +.alias-item { + display: flex; + justify-content: space-between; +} + ::v-deep .modal-container { display: block; overflow: scroll; diff --git a/src/components/AliasForm.vue b/src/components/AliasForm.vue new file mode 100644 index 000000000..a88df8803 --- /dev/null +++ b/src/components/AliasForm.vue @@ -0,0 +1,158 @@ +<!-- + - @copyright 2021 Daniel Kesselberg <mail@danielkesselberg.de> + - + - @author 2021 Daniel Kesselberg <mail@danielkesselberg.de> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - This program is distributed in the hope that it will be useful, + - but WITHOUT ANY WARRANTY; without even the implied warranty of + - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + - GNU Affero General Public License for more details. + - + - You should have received a copy of the GNU Affero General Public License + - along with this program. If not, see <http://www.gnu.org/licenses/>. + --> + +<template> + <div> + <form v-if="showForm" class="alias-form" @submit.prevent="updateAlias"> + <div> + <input v-model="changeName" + type="text" + required> + <input v-model="changeAlias" + type="email" + :disabled="alias.provisioned" + required> + </div> + <div class="button-group"> + <button + class="icon" + type="submit" + :class="loading ? 'icon-loading-small-dark' : 'icon-checkmark'" + :title="t('mail', 'Update alias')" /> + </div> + </form> + <div v-else class="alias-item"> + <p><strong>{{ alias.name }}</strong> <{{ alias.alias }}></p> + <div class="button-group"> + <button + class="icon icon-rename" + :title="t('mail', 'Show update alias form')" + @click="showForm = true" /> + <button v-if="!alias.provisioned" + class="icon" + :title="t('mail', 'Delete alias')" + :class="loading ? 'icon-loading-small-dark' : 'icon-delete'" + @click="deleteAlias" /> + </div> + </div> + </div> +</template> + +<script> +import logger from '../logger' + +export default { + name: 'AliasForm', + props: { + account: { + type: Object, + required: true, + }, + alias: { + type: Object, + required: true, + }, + }, + data() { + return { + changeAlias: this.alias.alias, + changeName: this.alias.name, + showForm: false, + loading: false, + } + }, + methods: { + async updateAlias(e) { + this.loading = true + + await this.$store.dispatch('updateAlias', { + account: this.account, + aliasId: this.alias.id, + alias: this.changeAlias, + name: this.changeName, + }) + + logger.debug('updated alias', { + accountId: this.account.id, + aliasId: this.alias.id, + alias: this.changeAlias, + name: this.changeName, + }) + + this.showForm = false + this.loading = false + }, + async deleteAlias() { + this.loading = true + + await this.$store.dispatch('deleteAlias', { + account: this.account, + aliasId: this.alias.id, + }) + + logger.debug('deleted alias', { + accountId: this.account.id, + aliasId: this.alias.id, + alias: this.alias.alias, + name: this.alias.name, + }) + + this.showForm = false + this.loading = false + }, + }, +} +</script> + +<style lang="scss" scoped> +.alias-form, .alias-item { + display: flex; + justify-content: space-between; + align-items: center; +} + +.button-group { + display: flex; + align-items: center; +} + +.icon { + background-color: var(--color-main-background); + border: none; + opacity: 0.7; + + &:hover, &:focus { + opacity: 1; + } +} + +.icon-checkmark { + background-image: var(--icon-checkmark-000); +} + +.icon-delete { + background-image: var(--icon-delete-000); +} + +.icon-rename { + background-image: var(--icon-rename-000); +} +</style> diff --git a/src/components/AliasSettings.vue b/src/components/AliasSettings.vue index c7fd79025..7f579d010 100644 --- a/src/components/AliasSettings.vue +++ b/src/components/AliasSettings.vue @@ -20,44 +20,40 @@ <template> <div> <ul class="aliases-list"> - <li v-for="curAlias in aliases" :key="curAlias.id"> - <strong>{{ curAlias.name }}</strong> <{{ curAlias.alias }}> - - <button class="icon-delete" @click="deleteAlias(curAlias)" /> + <li v-for="alias in aliases" :key="alias.id"> + <AliasForm :account="account" :alias="alias" /> + </li> + <li v-if="showForm"> + <form id="createAliasForm" @submit.prevent="createAlias"> + <input v-model="newName" + type="text" + :placeholder="t('mail', 'Name')" + required> + <input v-model="newAlias" + type="email" + :placeholder="t('mail', 'Email-Address')" + required> + </form> </li> </ul> - <div> - <input - v-if="addMode" - id="alias-name" - v-model="alias.aliasName" - type="text" - :placeholder="t('mail', 'Name')" - :disabled="loading"> - - <input - v-if="addMode" - id="alias" - ref="email" - v-model="alias.alias" - type="email" - :placeholder="t('mail', 'Mail Address')" - :disabled="loading"> - </div> - - <div> - <button v-if="!addMode" class="primary icon-add" @click="enabledAddMode"> + <div v-if="!account.provisioningId"> + <button v-if="!showForm" class="primary icon-add" @click="showForm = true"> {{ t('mail', 'Add alias') }} </button> - <button - v-if="addMode" + <button v-if="showForm" class="primary" :class="loading ? 'icon-loading-small-dark' : 'icon-checkmark-white'" - :disabled="loading" - @click="saveAlias"> - {{ t('mail', 'Save') }} + type="submit" + form="createAliasForm" + :disabled="loading"> + {{ t('mail', 'Create alias') }} + </button> + <button v-if="showForm" + class="button-text" + @click="resetCreate"> + {{ t("mail", "Cancel") }} </button> </div> </div> @@ -65,10 +61,11 @@ <script> import logger from '../logger' -import Vue from 'vue' +import AliasForm from './AliasForm' export default { name: 'AliasSettings', + components: { AliasForm }, props: { account: { type: Object, @@ -77,9 +74,10 @@ export default { }, data() { return { - addMode: false, + newAlias: '', + newName: this.account.name, + showForm: false, loading: false, - alias: { aliasName: this.account.name, alias: '' }, } }, computed: { @@ -88,25 +86,28 @@ export default { }, }, methods: { - enabledAddMode() { - this.addMode = true - Vue.nextTick(this.focusEmailField) - }, - focusEmailField() { - this.$refs.email.focus() - }, - async deleteAlias(alias) { + async createAlias() { this.loading = true - await this.$store.dispatch('deleteAlias', { account: this.account, aliasToDelete: alias }) - logger.info('alias deleted') + + await this.$store.dispatch('createAlias', { + account: this.account, + alias: this.newAlias, + name: this.newName, + }) + + logger.debug('created alias', { + accountId: this.account.id, + alias: this.newAlias, + name: this.newName, + }) + + this.resetCreate() this.loading = false }, - async saveAlias() { - this.loading = true - await this.$store.dispatch('createAlias', { account: this.account, aliasToAdd: this.alias }) - logger.info('alias added') - this.alias = { aliasName: this.account.name, alias: '' } - this.loading = false + resetCreate() { + this.newAlias = '' + this.newName = this.account.name + this.showForm = false }, }, } @@ -122,23 +123,23 @@ export default { left: 14px; } } -input { - width: 195px; -} -.aliases-list { - margin: 0.5rem 0rem; -} -.icon-delete { - vertical-align: bottom; - background-image: var(--icon-delete-000); - background-color: var(--color-main-background); + +.button-text { + background-color: transparent; border: none; - opacity: 0.7; + color: var(--color-text-maxcontrast); + font-weight: normal; + &:hover, &:focus { - opacity: 1; + color: var(--color-main-text); } } + +input { + width: 195px; +} + .icon-add { background-image: var(--icon-add-fff); } diff --git a/src/components/settings/ProvisioningSettings.vue b/src/components/settings/ProvisioningSettings.vue index 9b963940d..276d8a306 100644 --- a/src/components/settings/ProvisioningSettings.vue +++ b/src/components/settings/ProvisioningSettings.vue @@ -281,6 +281,36 @@ </div> </div> </div> + <div v-if="ldapAliasesIntegration" class="settings-group"> + <div class="group-title"> + {{ t('mail', 'LDAP aliases integration') }} + </div> + <div class="group-inputs"> + <div> + <input + :id="'mail-provision-ldap-aliases-provisioning' + setting.id" + v-model="ldapAliasesProvisioning" + type="checkbox" + class="checkbox"> + <label :for="'mail-provision-ldap-aliases-provisioning' + setting.id"> + {{ t('mail', 'Enable ldap aliases integration') }} + </label> + <p>{{ t('mail', 'The ldap aliases integration reads a attribute from the configured ldap directory to provision email aliases.') }}</p> + </div> + <div> + <label :for="'mail-provision-ldap-aliases-attribute' + setting.id"> + {{ t('mail', 'LDAP attribute for aliases') }}* + <br> + <input :id="'mail-provision-ldap-aliases-attribute' + setting.id" + v-model="ldapAliasesAttribute" + :disabled="loading" + :required="ldapAliasesProvisioning" + type="text"> + </label> + <p>{{ t('mail', 'A multi value attribute to provision email aliases. For each value an alias is created. Alias existing in Nextcloud but not in the ldap directory are deleted.') }}</p> + </div> + </div> + </div> <div class="settings-group"> <div class="group-title" /> <div class="group-inputs"> @@ -321,6 +351,9 @@ <script> import logger from '../../logger' import ProvisionPreview from './ProvisionPreview' +import { loadState } from '@nextcloud/initial-state' + +const ldapAliasesIntegration = loadState('mail', 'ldap_aliases_integration', false) export default { name: 'ProvisioningSettings', @@ -373,6 +406,9 @@ export default { uid: 'user321', email: 'user@domain.com', }, + ldapAliasesIntegration, + ldapAliasesProvisioning: this.setting.ldapAliasesProvisioning || false, + ldapAliasesAttribute: this.setting.ldapAliasesAttribute || '', loading: false, } }, @@ -394,6 +430,8 @@ export default { sieveHost: this.sieveHost, sievePort: this.sievePort, sieveSslMode: this.sieveSslMode, + ldapAliasesProvisioning: this.ldapAliasesProvisioning, + ldapAliasesAttribute: this.ldapAliasesAttribute, } }, }, @@ -423,6 +461,8 @@ export default { sieveHost: this.sieveHost, sievePort: this.sievePort, sieveSslMode: this.sieveSslMode, + ldapAliasesProvisioning: this.ldapAliasesProvisioning, + ldapAliasesAttribute: this.ldapAliasesAttribute, }) logger.info('provisioning setting updated') @@ -462,6 +502,7 @@ export default { .group-title { min-width: 100px; + max-width: 100px; text-align: right; margin: 10px; font-weight: bold; diff --git a/src/service/AliasService.js b/src/service/AliasService.js index 6b0cc15b3..0f3fcb0b5 100644 --- a/src/service/AliasService.js +++ b/src/service/AliasService.js @@ -1,29 +1,66 @@ import { generateUrl } from '@nextcloud/router' import axios from '@nextcloud/axios' -export const createAlias = async(account, data) => { +/** + * @typedef {Object} Alias + * @property {number} id + * @property {string} alias + * @property {string} name + * @property {string} signature + * @property {boolean} provisioned + */ + +/** + * @param {number} accountId id of account + * @param {string} alias new alias + * @param {string} aliasName new alias name + * @returns {Promise<Alias>} + */ +export const createAlias = async(accountId, alias, aliasName) => { const url = generateUrl('/apps/mail/api/accounts/{id}/aliases', { - id: account.accountId, + id: accountId, }) - return axios.post(url, data).then((resp) => resp.data).catch((e) => { - if (e.response && e.response.status === 400) { - throw e.response.data - } - - throw e - }) + return axios.post(url, { alias, aliasName }).then(resp => resp.data) } -export const deleteAlias = async(account, alias) => { +/** + * @param {number} accountId id of account + * @param {number} aliasId if of alias + * @returns {Promise<Alias>} + */ +export const deleteAlias = async(accountId, aliasId) => { const url = generateUrl('/apps/mail/api/accounts/{id}/aliases/{aliasId}', { - id: account.accountId, - aliasId: alias.id, + id: accountId, + aliasId, }) return axios.delete(url).then((resp) => resp.data) } +/** + * @param {number} accountId id of account + * @param {number} aliasId if of alias + * @param {string} alias new alias + * @param {string} aliasName new alias name + * @returns {Promise<Alias>} + */ +export const updateAlias = async(accountId, aliasId, alias, aliasName) => { + const url = generateUrl( + '/apps/mail/api/accounts/{id}/aliases/{aliasId}', { + id: accountId, + aliasId, + }) + + return axios.put(url, { alias, aliasName }).then(resp => resp.data) +} + +/** + * @param {number} accountId id of account + * @param {number} aliasId id of alias + * @param {string} signature new signature + * @returns {Promise<Alias>} + */ export const updateSignature = async(accountId, aliasId, signature) => { const url = generateUrl( '/apps/mail/api/accounts/{id}/aliases/{aliasId}/signature', { @@ -31,5 +68,5 @@ export const updateSignature = async(accountId, aliasId, signature) => { aliasId, }) - return axios.put(url, { signature }) + return axios.put(url, { signature }).then(resp => resp.data) } diff --git a/src/store/actions.js b/src/store/actions.js index 6e42c0402..5f00bdf0e 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -739,17 +739,36 @@ export default { throw err } }, - async createAlias({ commit }, { account, aliasToAdd }) { - const alias = await AliasService.createAlias(account, aliasToAdd) - commit('createAlias', { account, alias }) + async createAlias({ commit }, { account, alias, name }) { + const entity = await AliasService.createAlias(account.id, alias, name) + commit('createAlias', { + account, + alias: entity, + }) + }, + async deleteAlias({ commit }, { account, aliasId }) { + const entity = await AliasService.deleteAlias(account.id, aliasId) + commit('deleteAlias', { + account, + aliasId: entity.id, + }) }, - async deleteAlias({ commit }, { account, aliasToDelete }) { - await AliasService.deleteAlias(account, aliasToDelete) - commit('deleteAlias', { account, alias: aliasToDelete }) + async updateAlias({ commit }, { account, aliasId, alias, name }) { + const entity = await AliasService.updateAlias(account.id, aliasId, alias, name) + commit('patchAlias', { + account, + aliasId: entity.id, + data: { alias: entity.alias, name: entity.name }, + }) + commit('editAccount', account) }, async updateAliasSignature({ commit }, { account, aliasId, signature }) { - await AliasService.updateSignature(account.id, aliasId, signature) - commit('patchAlias', { account, aliasId, data: { signature } }) + const entity = await AliasService.updateSignature(account.id, aliasId, signature) + commit('patchAlias', { + account, + aliasId: entity.id, + data: { signature: entity.signature }, + }) commit('editAccount', account) }, async renameMailbox({ commit }, { account, mailbox, newName }) { diff --git a/src/store/mutations.js b/src/store/mutations.js index b9f10a823..b2efd6492 100644 --- a/src/store/mutations.js +++ b/src/store/mutations.js @@ -307,12 +307,17 @@ export default { createAlias(state, { account, alias }) { account.aliases.push(alias) }, - deleteAlias(state, { account, alias }) { - account.aliases.splice(account.aliases.indexOf(alias), 1) + deleteAlias(state, { account, aliasId }) { + const index = account.aliases.findIndex(temp => aliasId === temp.id) + if (index !== -1) { + account.aliases.splice(index, 1) + } }, patchAlias(state, { account, aliasId, data }) { - const index = account.aliases.findIndex((temp) => aliasId === temp.id) - account.aliases[index] = Object.assign({}, account.aliases[index], data) + const index = account.aliases.findIndex(temp => aliasId === temp.id) + if (index !== -1) { + account.aliases[index] = Object.assign({}, account.aliases[index], data) + } }, setMailboxUnreadCount(state, { id, unread }) { Vue.set(state.mailboxes[id], 'unread', unread ?? 0) diff --git a/tests/Unit/Controller/AliasesControllerTest.php b/tests/Unit/Controller/AliasesControllerTest.php index fa44ea4f0..81869237d 100644 --- a/tests/Unit/Controller/AliasesControllerTest.php +++ b/tests/Unit/Controller/AliasesControllerTest.php @@ -26,6 +26,7 @@ use OCA\Mail\Controller\AliasesController; use OCA\Mail\Db\Alias; use OCA\Mail\Db\AliasMapper; use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Exception\ClientException; use OCA\Mail\Exception\NotImplemented; use OCA\Mail\Service\AliasesService; use OCP\AppFramework\Db\DoesNotExistException; @@ -88,8 +89,54 @@ class AliasesControllerTest extends TestCase { } public function testUpdate(): void { - $this->expectException(NotImplemented::class); - $this->controller->update(); + $alias = new Alias(); + $alias->setId(101); + $alias->setAccountId(200); + $alias->setName('Jane Doe'); + $alias->setAlias('jane@doe.com'); + + $this->aliasMapper->expects($this->once()) + ->method('find') + ->with($alias->getId(), $this->userId) + ->willReturn($alias); + + $this->aliasMapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + + $response = $this->controller->update($alias->getId(), 'john@doe.com', 'John Doe'); + /** @var Alias $data */ + $data = $response->getData(); + + $this->assertInstanceOf(Alias::class, $response->getData()); + $this->assertEquals('john@doe.com', $data->getAlias()); + $this->assertEquals('John Doe', $data->getName()); + } + + public function testUpdateProvisioned(): void { + $alias = new Alias(); + $alias->setId(201); + $alias->setAccountId(300); + $alias->setName('Jane Doe'); + $alias->setAlias('jane@doe.com'); + $alias->setProvisioningId(100); + + $this->aliasMapper->expects($this->once()) + ->method('find') + ->with($alias->getId(), $this->userId) + ->willReturn($alias); + + $this->aliasMapper->expects($this->once()) + ->method('update') + ->willReturnArgument(0); + + $response = $this->controller->update($alias->getId(), 'john@doe.com', 'John Doe'); + /** @var Alias $data */ + $data = $response->getData(); + + $this->assertInstanceOf(Alias::class, $data); + $this->assertEquals('jane@doe.com', $data->getAlias()); + $this->assertEquals('John Doe', $data->getName()); } public function testDestroy(): void { @@ -106,7 +153,7 @@ class AliasesControllerTest extends TestCase { $this->aliasMapper->expects($this->once()) ->method('delete') - ->with($alias); + ->willReturnArgument(0); $expectedResponse = new JSONResponse($alias); $response = $this->controller->destroy($alias->getId()); @@ -114,6 +161,25 @@ class AliasesControllerTest extends TestCase { $this->assertEquals($expectedResponse, $response); } + public function testDestroyProvisioned(): void { + $this->expectException(ClientException::class); + $this->expectExceptionMessage('Deleting a provisioned alias is not allowed.'); + + $alias = new Alias(); + $alias->setId(201); + $alias->setAccountId(300); + $alias->setName('Jane Doe'); + $alias->setAlias('jane@doe.com'); + $alias->setProvisioningId(100); + + $this->aliasMapper->expects($this->once()) + ->method('find') + ->with($alias->getId(), $this->userId) + ->willReturn($alias); + + $this->controller->destroy($alias->getId()); + } + public function testCreate(): void { $alias = new Alias(); $alias->setId(102); @@ -172,10 +238,11 @@ class AliasesControllerTest extends TestCase { ->method('find') ->willReturn($alias); $this->aliasMapper->expects($this->once()) - ->method('update'); + ->method('update') + ->willReturnArgument(0); $expectedResponse = new JSONResponse( - [], + $alias, Http::STATUS_OK ); $response = $this->controller->updateSignature( diff --git a/tests/Unit/Service/AliasesServiceTest.php b/tests/Unit/Service/AliasesServiceTest.php index ddbe4ed04..17c5d8671 100644 --- a/tests/Unit/Service/AliasesServiceTest.php +++ b/tests/Unit/Service/AliasesServiceTest.php @@ -25,6 +25,7 @@ use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Mail\Db\Alias; use OCA\Mail\Db\AliasMapper; use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Exception\ClientException; use OCA\Mail\Service\AliasesService; use OCP\AppFramework\Db\DoesNotExistException; @@ -42,15 +43,11 @@ class AliasesServiceTest extends TestCase { /** @var MailAccountMapper */ private $mailAccountMapper; - /** @var Alias */ - private $alias; - protected function setUp(): void { parent::setUp(); $this->aliasMapper = $this->createMock(AliasMapper::class); $this->mailAccountMapper = $this->createMock(MailAccountMapper::class); - $this->alias = $this->createMock(Alias::class); $this->service = new AliasesService( $this->aliasMapper, @@ -58,36 +55,42 @@ class AliasesServiceTest extends TestCase { ); } - public function testFindAll() { - $accountId = 123; - $this->aliasMapper->expects($this->once()) + public function testFindAll(): void { + $entity = new Alias(); + $entity->setAccountId(200); + $entity->setAlias('jane@doe.com'); + $entity->setName('Jane Doe'); + + $this->aliasMapper->expects(self::once()) ->method('findAll') - ->with($accountId, $this->user) - ->will($this->returnValue([$this->alias])); + ->with($entity->getAccountId(), $this->user) + ->willReturn([$entity]); - $actual = $this->service->findAll($accountId, $this->user); + $aliases = $this->service->findAll($entity->getAccountId(), $this->user); - $expected = [ - $this->alias - ]; - $this->assertEquals($expected, $actual); + $this->assertEquals([$entity], $aliases); } - public function testFind() { - $aliasId = 123; - $this->aliasMapper->expects($this->once()) + public function testFind(): void { + $entity = new Alias(); + $entity->setId(101); + $entity->setAccountId(200); + $entity->setAlias('jane@doe.com'); + $entity->setName('Jane Doe'); + + $this->aliasMapper->expects(self::once()) ->method('find') - ->with($aliasId, $this->user) - ->will($this->returnValue($this->alias)); + ->with($entity->getId(), $this->user) + ->willReturn($entity); - $actual = $this->service->find($aliasId, $this->user); + $alias = $this->service->find($entity->getId(), $this->user); - $expected = $this->alias; - $this->assertEquals($expected, $actual); + $this->assertEquals($entity, $alias); } public function testCreate(): void { $entity = new Alias(); + $entity->setId(101); $entity->setAccountId(200); $entity->setAlias('jane@doe.com'); $entity->setName('Jane Doe'); @@ -95,7 +98,7 @@ class AliasesServiceTest extends TestCase { $this->mailAccountMapper->expects($this->once()) ->method('find'); - $this->aliasMapper->expects($this->once()) + $this->aliasMapper->expects(self::once()) ->method('insert') ->willReturnCallback(static function (Alias $alias) { $alias->setId(100); @@ -123,7 +126,7 @@ class AliasesServiceTest extends TestCase { $entity->setAlias('jane@doe.com'); $entity->setName('Jane Doe'); - $this->mailAccountMapper->expects($this->once()) + $this->mailAccountMapper->expects(self::once()) ->method('find') ->willThrowException(new DoesNotExistException('Account does not exist')); @@ -135,38 +138,70 @@ class AliasesServiceTest extends TestCase { ); } - public function testDelete() { - $aliasId = 123; - $this->aliasMapper->expects($this->once()) + public function testDelete(): void { + $entity = new Alias(); + $entity->setId(101); + $entity->setAccountId(200); + $entity->setName('Jane Doe'); + $entity->setAlias('jane@doe.com'); + + $this->aliasMapper->expects(self::once()) ->method('find') - ->with($aliasId, $this->user) - ->will($this->returnValue($this->alias)); - $this->aliasMapper->expects($this->once()) + ->with($entity->getId(), $this->user) + ->willReturn($entity); + $this->aliasMapper->expects(self::once()) ->method('delete') - ->with($this->alias); + ->willReturnArgument(0); - $this->service->delete($aliasId, $this->user); + $alias = $this->service->delete($this->user, $entity->getId()); + + $this->assertEquals($entity, $alias); + } + + public function testDeleteProvisioned(): void { + $this->expectException(ClientException::class); + $this->expectExceptionMessage('Deleting a provisioned alias is not allowed.'); + + $entity = new Alias(); + $entity->setId(201); + $entity->setAccountId(300); + $entity->setName('Jane Doe'); + $entity->setAlias('jane@doe.com'); + $entity->setProvisioningId(100); + + $this->aliasMapper->expects(self::once()) + ->method('find') + ->with($entity->getId(), $this->user) + ->willReturn($entity); + + $this->service->delete($this->user, $entity->getId()); } public function testUpdateSignature(): void { - $aliasId = 400; - $this->aliasMapper->expects($this->once()) + $entity = new Alias(); + $entity->setId(101); + $entity->setAccountId(200); + $entity->setName('Jane Doe'); + $entity->setAlias('jane@doe.com'); + + $this->aliasMapper->expects(self::once()) ->method('find') - ->with($aliasId, $this->user) - ->willReturn($this->alias); - $this->aliasMapper->expects($this->once()) - ->method('update'); + ->with($entity->getId(), $this->user) + ->willReturn($entity); + $this->aliasMapper->expects(self::once()) + ->method('update') + ->willReturnArgument(0); - $this->service->updateSignature($this->user, $aliasId, 'Kind regards<br>Herbert'); + $this->service->updateSignature($this->user, $entity->getId(), 'Kind regards<br>Herbert'); } public function testUpateSignatureInvalidAliasId(): void { $this->expectException(DoesNotExistException::class); - $this->aliasMapper->expects($this->once()) + $this->aliasMapper->expects(self::once()) ->method('find') ->willThrowException(new DoesNotExistException('Alias does not exist')); - $this->aliasMapper->expects($this->never()) + $this->aliasMapper->expects(self::never()) ->method('update'); $this->service->updateSignature($this->user, '999999', 'Kind regards<br>Herbert'); |