diff --git a/appinfo/info.xml b/appinfo/info.xml
index 92f33f3427..98ec76f017 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -85,6 +85,7 @@ Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud
OCA\Mail\Command\DiagnoseAccount
OCA\Mail\Command\ExportAccount
OCA\Mail\Command\ExportAccountThreads
+ OCA\Mail\Command\IonosCreateAccount
OCA\Mail\Command\PredictImportance
OCA\Mail\Command\SyncAccount
OCA\Mail\Command\Thread
diff --git a/composer.json b/composer.json
index 90a68468f4..03c8fabdc5 100644
--- a/composer.json
+++ b/composer.json
@@ -54,7 +54,7 @@
"source": {
"type": "git",
"url": "https://github.com/ionos-productivity/ionos-mail-configuration-api-client.git",
- "reference": "2.0.0-20251110130214"
+ "reference": "2.0.0-20251208083401"
},
"autoload": {
"psr-4": {
diff --git a/composer.lock b/composer.lock
index 301d6b3bf5..d99348d985 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "5341c5725717dffc9990db93ad12ab21",
+ "content-hash": "fb553591efe3fd5dbaed693076de60c6",
"packages": [
{
"name": "amphp/amp",
@@ -1860,7 +1860,7 @@
"source": {
"type": "git",
"url": "https://github.com/ionos-productivity/ionos-mail-configuration-api-client.git",
- "reference": "2.0.0-20251110130214"
+ "reference": "2.0.0-20251208083401"
},
"type": "library",
"autoload": {
diff --git a/lib/Command/IonosCreateAccount.php b/lib/Command/IonosCreateAccount.php
new file mode 100644
index 0000000000..da9d495007
--- /dev/null
+++ b/lib/Command/IonosCreateAccount.php
@@ -0,0 +1,212 @@
+setName('mail:ionos:create');
+ $this->setDescription('Creates IONOS mail account and configure it in Nextcloud');
+ $this->addArgument(self::ARGUMENT_USER_ID, InputArgument::REQUIRED, 'User ID');
+ $this->addArgument(self::ARGUMENT_EMAIL_USER, InputArgument::REQUIRED, 'IONOS Email user. (The local part of the email address before @domain)');
+ $this->addOption(self::OPTION_NAME, '', InputOption::VALUE_REQUIRED, 'Account name');
+ $this->addOption(self::OPTION_OUTPUT, '', InputOption::VALUE_OPTIONAL, 'Output format (json, json_pretty)');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $userId = $input->getArgument(self::ARGUMENT_USER_ID);
+ $emailUser = $input->getArgument(self::ARGUMENT_EMAIL_USER);
+ $name = $input->getOption(self::OPTION_NAME);
+ $outputFormat = $input->getOption(self::OPTION_OUTPUT);
+ $isJsonOutput = in_array($outputFormat, ['json', 'json_pretty'], true);
+
+ // Preflight checks
+ if (!$isJsonOutput) {
+ $output->writeln('Running preflight checks...');
+ }
+
+ // Check if IONOS integration is enabled and configured
+ if (!$this->configService->isIonosIntegrationEnabled()) {
+ if ($isJsonOutput) {
+ $this->outputJson($output, [
+ 'success' => false,
+ 'error' => 'IONOS integration is not enabled or not properly configured',
+ 'details' => [
+ 'ionos-mailconfig-enabled' => 'must be set to "yes"',
+ 'ionos_mailconfig_api_base_url' => 'must be configured',
+ 'ionos_mailconfig_api_auth_user' => 'must be configured',
+ 'ionos_mailconfig_api_auth_pass' => 'must be configured',
+ 'ncw.ext_ref' => 'must be configured in system config',
+ ]
+ ], $outputFormat);
+ } else {
+ $output->writeln('IONOS integration is not enabled or not properly configured');
+ $output->writeln('Please verify the following configuration:');
+ $output->writeln(' - ionos-mailconfig-enabled is set to "yes"');
+ $output->writeln(' - ionos_mailconfig_api_base_url is configured');
+ $output->writeln(' - ionos_mailconfig_api_auth_user is configured');
+ $output->writeln(' - ionos_mailconfig_api_auth_pass is configured');
+ $output->writeln(' - ncw.ext_ref is configured in system config');
+ }
+ return 1;
+ }
+
+ // Get and display the mail domain
+ $mailDomain = $this->configService->getMailDomain();
+ if (empty($mailDomain)) {
+ if ($isJsonOutput) {
+ $this->outputJson($output, [
+ 'success' => false,
+ 'error' => 'Mail domain could not be determined',
+ 'details' => 'Please verify ncw.customerDomain is configured in system config'
+ ], $outputFormat);
+ } else {
+ $output->writeln('Mail domain could not be determined');
+ $output->writeln('Please verify ncw.customerDomain is configured in system config');
+ }
+ return 1;
+ }
+
+ if (!$isJsonOutput) {
+ $output->writeln('✓ IONOS API is properly configured');
+ $output->writeln('✓ Mail domain: ' . $mailDomain . '');
+ $output->writeln('');
+ }
+
+ if (!$this->userManager->userExists($userId)) {
+ if ($isJsonOutput) {
+ $this->outputJson($output, [
+ 'success' => false,
+ 'error' => 'User does not exist',
+ 'userId' => $userId
+ ], $outputFormat);
+ } else {
+ $output->writeln("User $userId does not exist");
+ }
+ return 1;
+ }
+
+ if (!$isJsonOutput) {
+ $output->writeln('Creating IONOS mail account...');
+ $output->writeln(' user-id: ' . $userId);
+ $output->writeln(' name: ' . $name);
+ $output->writeln(' email-user: ' . $emailUser);
+ $output->writeln(' full-email: ' . $emailUser . '@' . $mailDomain);
+ }
+
+ try {
+ $this->logger->info('Starting IONOS email account creation from CLI', [
+ 'userId' => $userId,
+ 'emailUser' => $emailUser,
+ 'accountName' => $name
+ ]);
+
+ // Use the shared account creation service
+ $account = $this->accountCreationService->createOrUpdateAccount($userId, $emailUser, $name);
+
+ if ($isJsonOutput) {
+ $this->outputJson($output, [
+ 'success' => true,
+ 'account' => [
+ 'id' => $account->getId(),
+ 'userId' => $account->getUserId(),
+ 'name' => $account->getName(),
+ 'email' => $account->getEmail(),
+ 'inbound' => [
+ 'host' => $account->getInboundHost(),
+ 'port' => $account->getInboundPort(),
+ 'sslMode' => $account->getInboundSslMode(),
+ 'user' => $account->getInboundUser(),
+ ],
+ 'outbound' => [
+ 'host' => $account->getOutboundHost(),
+ 'port' => $account->getOutboundPort(),
+ 'sslMode' => $account->getOutboundSslMode(),
+ 'user' => $account->getOutboundUser(),
+ ]
+ ]
+ ], $outputFormat);
+ } else {
+ $output->writeln('Account created successfully!');
+ $output->writeln(' Account ID: ' . $account->getId());
+ $output->writeln(' Email: ' . $account->getEmail());
+ }
+
+ return 0;
+ } catch (ServiceException $e) {
+ $this->logger->error('IONOS service error: ' . $e->getMessage(), [
+ 'statusCode' => $e->getCode()
+ ]);
+
+ if ($isJsonOutput) {
+ $this->outputJson($output, [
+ 'success' => false,
+ 'error' => 'IONOS service error',
+ 'message' => $e->getMessage(),
+ 'statusCode' => $e->getCode()
+ ], $outputFormat);
+ } else {
+ $output->writeln('Failed to create IONOS account: ' . $e->getMessage() . '');
+ }
+ return 1;
+ } catch (\Exception $e) {
+ $this->logger->error('Unexpected error creating account: ' . $e->getMessage());
+
+ if ($isJsonOutput) {
+ $this->outputJson($output, [
+ 'success' => false,
+ 'error' => 'Unexpected error',
+ 'message' => $e->getMessage()
+ ], $outputFormat);
+ } else {
+ $output->writeln('Could not create account: ' . $e->getMessage() . '');
+ }
+ return 1;
+ }
+ }
+
+ /**
+ * Output data as JSON based on the specified format
+ */
+ private function outputJson(OutputInterface $output, array $data, ?string $format): void {
+ if ($format === 'json_pretty') {
+ $output->writeln(json_encode($data, JSON_PRETTY_PRINT));
+ } else {
+ $output->writeln(json_encode($data));
+ }
+ }
+}
diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php
index ea7c02de14..e2e2552564 100644
--- a/lib/Controller/AccountsController.php
+++ b/lib/Controller/AccountsController.php
@@ -346,7 +346,8 @@ public function create(string $accountName,
?string $smtpSslMode = null,
?string $smtpUser = null,
?string $smtpPassword = null,
- string $authMethod = 'password'): JSONResponse {
+ string $authMethod = 'password',
+ bool $skipConnectivityTest = false): JSONResponse {
if ($this->config->getAppValue(Application::APP_ID, 'allow_new_mail_accounts', 'yes') === 'no') {
$this->logger->info('Creating account disabled by admin.');
return MailJsonResponse::error('Could not create account');
@@ -378,7 +379,7 @@ public function create(string $accountName,
);
}
try {
- $account = $this->setup->createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->currentUserId, $authMethod);
+ $account = $this->setup->createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->currentUserId, $authMethod, null, $skipConnectivityTest);
} catch (CouldNotConnectException $e) {
$data = [
'error' => $e->getReason(),
diff --git a/lib/Controller/IonosAccountsController.php b/lib/Controller/IonosAccountsController.php
index 60b647296e..06706462a0 100644
--- a/lib/Controller/IonosAccountsController.php
+++ b/lib/Controller/IonosAccountsController.php
@@ -11,12 +11,12 @@
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\JsonResponse as MailJsonResponse;
use OCA\Mail\Http\TrapError;
-use OCA\Mail\Service\IONOS\Dto\MailAccountConfig;
-use OCA\Mail\Service\IONOS\IonosMailService;
+use OCA\Mail\Service\IONOS\IonosAccountCreationService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
+use OCP\IUserSession;
use Psr\Log\LoggerInterface;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
@@ -29,8 +29,8 @@ class IonosAccountsController extends Controller {
public function __construct(
string $appName,
IRequest $request,
- private IonosMailService $ionosMailService,
- private AccountsController $accountsController,
+ private IonosAccountCreationService $accountCreationService,
+ private IUserSession $userSession,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
@@ -53,42 +53,72 @@ public function create(string $accountName, string $emailUser): JSONResponse {
return $error;
}
+ $userId = $this->getUserIdOrFail();
+ if ($userId instanceof JSONResponse) {
+ return $userId;
+ }
+
try {
- $this->logger->info('Starting IONOS email account creation', [ 'emailAddress' => $emailUser, 'accountName' => $accountName ]);
- $ionosResponse = $this->ionosMailService->createEmailAccount($emailUser);
+ $this->logger->info('Starting IONOS email account creation from web', [
+ 'userId' => $userId,
+ 'emailUser' => $emailUser,
+ 'accountName' => $accountName
+ ]);
+
+ // Use the shared account creation service
+ $account = $this->accountCreationService->createOrUpdateAccount($userId, $emailUser, $accountName);
- $this->logger->info('IONOS email account created successfully', [ 'emailAddress' => $ionosResponse->getEmail() ]);
- return $this->createNextcloudMailAccount($accountName, $ionosResponse);
+ $this->logger->info('Account creation completed successfully', [
+ 'accountId' => $account->getId(),
+ 'emailAddress' => $account->getEmail(),
+ 'userId' => $userId,
+ ]);
+
+ return new JSONResponse([
+ 'id' => $account->getId(),
+ 'accountName' => $account->getName(),
+ 'emailAddress' => $account->getEmail(),
+ ]);
} catch (ServiceException $e) {
+ return $this->buildServiceErrorResponse($e, 'account creation');
+ } catch (\Exception $e) {
+ $this->logger->error('Unexpected error during account creation: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'userId' => $userId,
+ ]);
+ return MailJsonResponse::error('Could not create account');
+ }
+ }
+
+ /**
+ * Get the current user ID or return error response
+ *
+ * @return string|JSONResponse User ID string or error response
+ */
+ private function getUserIdOrFail(): string|JSONResponse {
+ $user = $this->userSession->getUser();
+ if ($user === null) {
$data = [
'error' => self::ERR_IONOS_API_ERROR,
- 'statusCode' => $e->getCode(),
+ 'statusCode' => 401,
+ 'message' => 'No user session found',
];
- $this->logger->error('IONOS service error: ' . $e->getMessage(), $data);
-
+ $this->logger->error('No user session found during account creation', $data);
return MailJsonResponse::fail($data);
- } catch (\Exception $e) {
- return MailJsonResponse::error('Could not create account');
}
+ return $user->getUID();
}
- private function createNextcloudMailAccount(string $accountName, MailAccountConfig $mailConfig): JSONResponse {
- $imap = $mailConfig->getImap();
- $smtp = $mailConfig->getSmtp();
-
- return $this->accountsController->create(
- $accountName,
- $mailConfig->getEmail(),
- $imap->getHost(),
- $imap->getPort(),
- $imap->getSecurity(),
- $imap->getUsername(),
- $imap->getPassword(),
- $smtp->getHost(),
- $smtp->getPort(),
- $smtp->getSecurity(),
- $smtp->getUsername(),
- $smtp->getPassword(),
- );
+ /**
+ * Build service error response
+ */
+ private function buildServiceErrorResponse(ServiceException $e, string $context): JSONResponse {
+ $data = [
+ 'error' => self::ERR_IONOS_API_ERROR,
+ 'statusCode' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ ];
+ $this->logger->error('IONOS service error during ' . $context . ': ' . $e->getMessage(), $data);
+ return MailJsonResponse::fail($data);
}
}
diff --git a/lib/Service/IONOS/ConflictResolutionResult.php b/lib/Service/IONOS/ConflictResolutionResult.php
new file mode 100644
index 0000000000..f1a402c10e
--- /dev/null
+++ b/lib/Service/IONOS/ConflictResolutionResult.php
@@ -0,0 +1,81 @@
+canRetry;
+ }
+
+ public function getAccountConfig(): ?MailAccountConfig {
+ return $this->accountConfig;
+ }
+
+ public function hasEmailMismatch(): bool {
+ return $this->expectedEmail !== null && $this->existingEmail !== null;
+ }
+
+ public function getExpectedEmail(): ?string {
+ return $this->expectedEmail;
+ }
+
+ public function getExistingEmail(): ?string {
+ return $this->existingEmail;
+ }
+}
diff --git a/lib/Service/IONOS/Dto/MailAccountConfig.php b/lib/Service/IONOS/Dto/MailAccountConfig.php
index 2a4c3a5fa5..3ac69a446f 100644
--- a/lib/Service/IONOS/Dto/MailAccountConfig.php
+++ b/lib/Service/IONOS/Dto/MailAccountConfig.php
@@ -32,6 +32,20 @@ public function getSmtp(): MailServerConfig {
return $this->smtp;
}
+ /**
+ * Create a new instance with updated passwords for both IMAP and SMTP
+ *
+ * @param string $newPassword The new password to use
+ * @return self New instance with updated passwords
+ */
+ public function withPassword(string $newPassword): self {
+ return new self(
+ email: $this->email,
+ imap: $this->imap->withPassword($newPassword),
+ smtp: $this->smtp->withPassword($newPassword),
+ );
+ }
+
/**
* Convert to array format for backwards compatibility
*
diff --git a/lib/Service/IONOS/Dto/MailServerConfig.php b/lib/Service/IONOS/Dto/MailServerConfig.php
index 2e06d9073d..27b5cb7aed 100644
--- a/lib/Service/IONOS/Dto/MailServerConfig.php
+++ b/lib/Service/IONOS/Dto/MailServerConfig.php
@@ -42,6 +42,22 @@ public function getPassword(): string {
return $this->password;
}
+ /**
+ * Create a new instance with a different password
+ *
+ * @param string $newPassword The new password to use
+ * @return self New instance with updated password
+ */
+ public function withPassword(string $newPassword): self {
+ return new self(
+ host: $this->host,
+ port: $this->port,
+ security: $this->security,
+ username: $this->username,
+ password: $newPassword,
+ );
+ }
+
/**
* Convert to array format for backwards compatibility
*
diff --git a/lib/Service/IONOS/IonosAccountConflictResolver.php b/lib/Service/IONOS/IonosAccountConflictResolver.php
new file mode 100644
index 0000000000..9b77f3913a
--- /dev/null
+++ b/lib/Service/IONOS/IonosAccountConflictResolver.php
@@ -0,0 +1,74 @@
+ionosMailService->getAccountConfigForUser($userId);
+
+ if ($ionosConfig === null) {
+ $this->logger->debug('No existing IONOS account found for conflict resolution', [
+ 'userId' => $userId
+ ]);
+ return ConflictResolutionResult::noExistingAccount();
+ }
+
+ // Construct full email address from username to compare with existing account
+ $domain = $this->ionosConfigService->getMailDomain();
+ $expectedEmail = $emailUser . '@' . $domain;
+
+ // Ensure the retrieved email matches the requested email
+ if ($ionosConfig->getEmail() === $expectedEmail) {
+ $this->logger->info('IONOS account already exists, retrieving new password for retry', [
+ 'emailAddress' => $ionosConfig->getEmail(),
+ 'userId' => $userId
+ ]);
+
+ // Get fresh password via resetAppPassword API since getAccountConfigForUser
+ // does not return password for security reasons
+ $newPassword = $this->ionosMailService->resetAppPassword($userId, IonosConfigService::APP_NAME);
+
+ // Create new config with the fresh password
+ $configWithPassword = $ionosConfig->withPassword($newPassword);
+
+ return ConflictResolutionResult::retry($configWithPassword);
+ }
+
+ $this->logger->warning('IONOS account exists but email mismatch', [
+ 'requestedEmail' => $expectedEmail,
+ 'existingEmail' => $ionosConfig->getEmail(),
+ 'userId' => $userId
+ ]);
+
+ return ConflictResolutionResult::emailMismatch($expectedEmail, $ionosConfig->getEmail());
+ }
+}
diff --git a/lib/Service/IONOS/IonosAccountCreationService.php b/lib/Service/IONOS/IonosAccountCreationService.php
new file mode 100644
index 0000000000..ffbd354b95
--- /dev/null
+++ b/lib/Service/IONOS/IonosAccountCreationService.php
@@ -0,0 +1,205 @@
+buildEmailAddress($emailUser);
+
+ // Check if Nextcloud account already exists
+ $existingAccounts = $this->accountService->findByUserIdAndAddress($userId, $expectedEmail);
+
+ if (!empty($existingAccounts)) {
+ return $this->handleExistingAccount($userId, $emailUser, $accountName, $existingAccounts[0]);
+ }
+
+ // No existing account - create new one
+ return $this->handleNewAccount($userId, $emailUser, $accountName);
+ }
+
+ /**
+ * Handle the case where a Nextcloud mail account already exists
+ */
+ private function handleExistingAccount(string $userId, string $emailUser, string $accountName, $existingAccount): MailAccount {
+ $this->logger->info('Nextcloud mail account already exists, resetting credentials', [
+ 'accountId' => $existingAccount->getId(),
+ 'emailAddress' => $existingAccount->getEmail(),
+ 'userId' => $userId,
+ ]);
+
+ try {
+ $resolutionResult = $this->conflictResolver->resolveConflict($userId, $emailUser);
+
+ if (!$resolutionResult->canRetry()) {
+ if ($resolutionResult->hasEmailMismatch()) {
+ throw new ServiceException(
+ 'IONOS account exists but email mismatch. Expected: ' . $resolutionResult->getExpectedEmail() . ', Found: ' . $resolutionResult->getExistingEmail(),
+ 409
+ );
+ }
+ throw new ServiceException('Nextcloud account exists but no IONOS account found', 500);
+ }
+
+ $mailConfig = $resolutionResult->getAccountConfig();
+ return $this->updateAccount($existingAccount->getMailAccount(), $accountName, $mailConfig);
+ } catch (ServiceException $e) {
+ throw new ServiceException('Failed to reset IONOS account credentials: ' . $e->getMessage(), $e->getCode(), $e);
+ }
+ }
+
+ /**
+ * Handle the case where no Nextcloud account exists yet
+ */
+ private function handleNewAccount(string $userId, string $emailUser, string $accountName): MailAccount {
+ try {
+ $this->logger->info('Creating new IONOS email account', [
+ 'userId' => $userId,
+ 'emailUser' => $emailUser,
+ 'accountName' => $accountName
+ ]);
+
+ $mailConfig = $this->ionosMailService->createEmailAccountForUser($userId, $emailUser);
+
+ $this->logger->info('IONOS email account created successfully', [
+ 'emailAddress' => $mailConfig->getEmail()
+ ]);
+
+ return $this->createAccount($userId, $accountName, $mailConfig);
+ } catch (ServiceException $e) {
+ // Try to resolve conflict - IONOS account might already exist
+ $this->logger->info('IONOS account creation failed, attempting conflict resolution', [
+ 'userId' => $userId,
+ 'emailUser' => $emailUser,
+ 'error' => $e->getMessage()
+ ]);
+
+ $resolutionResult = $this->conflictResolver->resolveConflict($userId, $emailUser);
+
+ if (!$resolutionResult->canRetry()) {
+ if ($resolutionResult->hasEmailMismatch()) {
+ throw new ServiceException(
+ 'IONOS account exists but email mismatch. Expected: ' . $resolutionResult->getExpectedEmail() . ', Found: ' . $resolutionResult->getExistingEmail(),
+ 409,
+ $e
+ );
+ }
+ // No existing IONOS account found - re-throw original error
+ throw $e;
+ }
+
+ $mailConfig = $resolutionResult->getAccountConfig();
+ return $this->createAccount($userId, $accountName, $mailConfig);
+ }
+ }
+
+ /**
+ * Create a new Nextcloud mail account
+ */
+ private function createAccount(string $userId, string $accountName, MailAccountConfig $mailConfig): MailAccount {
+ $account = new MailAccount();
+ $account->setUserId($userId);
+ $account->setName($accountName);
+ $account->setEmail($mailConfig->getEmail());
+ $account->setAuthMethod('password');
+
+ $this->setAccountCredentials($account, $mailConfig);
+
+ $account = $this->accountService->save($account);
+
+ $this->logger->info('Created new Nextcloud mail account', [
+ 'accountId' => $account->getId(),
+ 'emailAddress' => $account->getEmail(),
+ 'userId' => $userId,
+ ]);
+
+ return $account;
+ }
+
+ /**
+ * Update an existing Nextcloud mail account
+ */
+ private function updateAccount(MailAccount $account, string $accountName, MailAccountConfig $mailConfig): MailAccount {
+ $account->setName($accountName);
+ $this->setAccountCredentials($account, $mailConfig);
+
+ $account = $this->accountService->update($account);
+
+ $this->logger->info('Updated existing Nextcloud mail account with new credentials', [
+ 'accountId' => $account->getId(),
+ 'emailAddress' => $account->getEmail(),
+ 'userId' => $account->getUserId(),
+ ]);
+
+ return $account;
+ }
+
+ /**
+ * Set IMAP and SMTP credentials on a mail account
+ */
+ private function setAccountCredentials(MailAccount $account, MailAccountConfig $mailConfig): void {
+ $imap = $mailConfig->getImap();
+ $account->setInboundHost($imap->getHost());
+ $account->setInboundPort($imap->getPort());
+ $account->setInboundSslMode($imap->getSecurity());
+ $account->setInboundUser($imap->getUsername());
+ $account->setInboundPassword($this->crypto->encrypt($imap->getPassword()));
+
+ $smtp = $mailConfig->getSmtp();
+ $account->setOutboundHost($smtp->getHost());
+ $account->setOutboundPort($smtp->getPort());
+ $account->setOutboundSslMode($smtp->getSecurity());
+ $account->setOutboundUser($smtp->getUsername());
+ $account->setOutboundPassword($this->crypto->encrypt($smtp->getPassword()));
+ }
+
+ /**
+ * Build full email address from username
+ */
+ private function buildEmailAddress(string $emailUser): string {
+ $domain = $this->ionosMailService->getMailDomain();
+ return $emailUser . '@' . $domain;
+ }
+}
diff --git a/lib/Service/IONOS/IonosConfigService.php b/lib/Service/IONOS/IonosConfigService.php
index 3b5ff579ee..d72f57f99d 100644
--- a/lib/Service/IONOS/IonosConfigService.php
+++ b/lib/Service/IONOS/IonosConfigService.php
@@ -22,6 +22,11 @@
* Service for managing IONOS API configuration
*/
class IonosConfigService {
+ /**
+ * Application name used for IONOS app password management
+ */
+ public const APP_NAME = 'NEXTCLOUD_WORKSPACE';
+
public function __construct(
private readonly IConfig $config,
private readonly IAppConfig $appConfig,
diff --git a/lib/Service/IONOS/IonosMailConfigService.php b/lib/Service/IONOS/IonosMailConfigService.php
index 77e9b8126e..9e4b7a6706 100644
--- a/lib/Service/IONOS/IonosMailConfigService.php
+++ b/lib/Service/IONOS/IonosMailConfigService.php
@@ -9,6 +9,8 @@
namespace OCA\Mail\Service\IONOS;
+use OCA\Mail\Service\AccountService;
+use OCP\IUserSession;
use Psr\Log\LoggerInterface;
/**
@@ -18,6 +20,8 @@ class IonosMailConfigService {
public function __construct(
private IonosConfigService $ionosConfigService,
private IonosMailService $ionosMailService,
+ private AccountService $accountService,
+ private IUserSession $userSession,
private LoggerInterface $logger,
) {
}
@@ -27,7 +31,8 @@ public function __construct(
*
* The configuration is available only if:
* 1. The IONOS integration is enabled and properly configured
- * 2. The user does NOT already have an IONOS mail account
+ * 2. The user does NOT already have an IONOS mail account configured remotely
+ * 3. OR the user has a remote IONOS account but it's NOT configured locally in the mail app
*
* @return bool True if mail configuration should be shown, false otherwise
*/
@@ -38,14 +43,45 @@ public function isMailConfigAvailable(): bool {
return false;
}
- // Check if user already has an account
- $userHasAccount = $this->ionosMailService->mailAccountExistsForCurrentUser();
+ // Get current user
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ $this->logger->debug('IONOS mail config not available - no user session');
+ return false;
+ }
+ $userId = $user->getUID();
+
+ // Check if user already has a remote IONOS account
+ $userHasRemoteAccount = $this->ionosMailService->mailAccountExistsForCurrentUser();
+
+ if (!$userHasRemoteAccount) {
+ // No remote account exists, configuration should be available
+ return true;
+ }
- if ($userHasAccount) {
- $this->logger->debug('IONOS mail config not available - user already has an account');
+ // User has a remote account, check if it's configured locally
+ $ionosEmail = $this->ionosMailService->getIonosEmailForUser($userId);
+ if ($ionosEmail === null) {
+ // This shouldn't happen if userHasRemoteAccount is true, but handle it gracefully
+ $this->logger->warning('IONOS remote account exists but email could not be retrieved');
return false;
}
+ // Check if the IONOS email is configured in the local mail app
+ $localAccounts = $this->accountService->findByUserIdAndAddress($userId, $ionosEmail);
+ $hasLocalAccount = count($localAccounts) > 0;
+
+ if ($hasLocalAccount) {
+ $this->logger->debug('IONOS mail config not available - user already has account configured locally', [
+ 'email' => $ionosEmail,
+ ]);
+ return false;
+ }
+
+ // Remote account exists but not configured locally - show configuration
+ $this->logger->debug('IONOS mail config available - remote account exists but not configured locally', [
+ 'email' => $ionosEmail,
+ ]);
return true;
} catch (\Exception $e) {
$this->logger->error('Error checking IONOS mail config availability', [
diff --git a/lib/Service/IONOS/IonosMailService.php b/lib/Service/IONOS/IonosMailService.php
index 59279b6ee9..6de51f7ccb 100644
--- a/lib/Service/IONOS/IonosMailService.php
+++ b/lib/Service/IONOS/IonosMailService.php
@@ -11,9 +11,12 @@
use IONOS\MailConfigurationAPI\Client\Api\MailConfigurationAPIApi;
use IONOS\MailConfigurationAPI\Client\ApiException;
+use IONOS\MailConfigurationAPI\Client\Model\Imap;
+use IONOS\MailConfigurationAPI\Client\Model\MailAccountCreatedResponse;
use IONOS\MailConfigurationAPI\Client\Model\MailAccountResponse;
use IONOS\MailConfigurationAPI\Client\Model\MailAddonErrorMessage;
use IONOS\MailConfigurationAPI\Client\Model\MailCreateData;
+use IONOS\MailConfigurationAPI\Client\Model\Smtp;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Service\IONOS\Dto\MailAccountConfig;
use OCA\Mail\Service\IONOS\Dto\MailServerConfig;
@@ -119,21 +122,39 @@ private function getMailAccountResponse(string $userId): ?MailAccountResponse {
}
/**
- * Create an IONOS email account via API
+ * Create an IONOS email account via API for the current logged-in user
*
+ * @param string $userName The local part of the email address (before @domain)
* @return MailAccountConfig Mail account configuration
* @throws ServiceException
* @throws AppConfigException
*/
public function createEmailAccount(string $userName): MailAccountConfig {
$userId = $this->getCurrentUserId();
+ return $this->createEmailAccountForUser($userId, $userName);
+ }
+
+ /**
+ * Create an IONOS email account via API for a specific user
+ *
+ * This method allows creating email accounts without relying on the user session,
+ * making it suitable for use in OCC commands or admin operations.
+ *
+ * @param string $userId The Nextcloud user ID
+ * @param string $userName The local part of the email address (before @domain)
+ * @return MailAccountConfig Mail account configuration
+ * @throws ServiceException
+ * @throws AppConfigException
+ */
+ public function createEmailAccountForUser(string $userId, string $userName): MailAccountConfig {
$domain = $this->configService->getMailDomain();
$this->logger->debug('Sending request to mailconfig service', [
'extRef' => $this->configService->getExternalReference(),
'userName' => $userName,
'domain' => $domain,
- 'apiBaseUrl' => $this->configService->getApiBaseUrl()
+ 'apiBaseUrl' => $this->configService->getApiBaseUrl(),
+ 'userId' => $userId
]);
$apiInstance = $this->createApiInstance();
@@ -164,7 +185,7 @@ public function createEmailAccount(string $userName): MailAccountConfig {
]);
throw new ServiceException('Failed to create ionos mail', $result->getStatus());
}
- if ($result instanceof MailAccountResponse) {
+ if ($result instanceof MailAccountCreatedResponse) {
$this->logger->info('Successfully created IONOS mail account', [
'email' => $result->getEmail(),
'userId' => $userId,
@@ -199,6 +220,45 @@ public function createEmailAccount(string $userName): MailAccountConfig {
}
}
+ /**
+ * Get IONOS account configuration for a specific user
+ *
+ * This method retrieves the configuration of an existing IONOS mail account.
+ * Useful when an account was previously created but Nextcloud account creation failed.
+ *
+ * @param string $userId The Nextcloud user ID
+ * @return MailAccountConfig|null Mail account configuration if exists, null otherwise
+ * @throws ServiceException
+ */
+ public function getAccountConfigForUser(string $userId): ?MailAccountConfig {
+ $response = $this->getMailAccountResponse($userId);
+
+ if ($response === null) {
+ $this->logger->debug('No existing IONOS account found for user', [
+ 'userId' => $userId
+ ]);
+ return null;
+ }
+
+ $this->logger->info('Retrieved existing IONOS account configuration', [
+ 'email' => $response->getEmail(),
+ 'userId' => $userId
+ ]);
+
+ return $this->buildConfigFromAccountResponse($response);
+ }
+
+ /**
+ * Get IONOS account configuration for the current logged-in user
+ *
+ * @return MailAccountConfig|null Mail account configuration if exists, null otherwise
+ * @throws ServiceException
+ */
+ public function getAccountConfigForCurrentUser(): ?MailAccountConfig {
+ $userId = $this->getCurrentUserId();
+ return $this->getAccountConfigForUser($userId);
+ }
+
/**
* Get the current user ID
*
@@ -255,38 +315,71 @@ private function normalizeSslMode(string $apiSslMode): string {
}
/**
- * Build success response with mail configuration
+ * Build mail account configuration from server details
*
- * @param MailAccountResponse $response
- * @return MailAccountConfig
+ * @param Imap $imapServer IMAP server configuration object
+ * @param Smtp $smtpServer SMTP server configuration object
+ * @param string $email Email address
+ * @param string $password Account password
+ * @return MailAccountConfig Complete mail account configuration
*/
- private function buildSuccessResponse(MailAccountResponse $response): MailAccountConfig {
- $smtpServer = $response->getServer()->getSmtp();
- $imapServer = $response->getServer()->getImap();
-
+ private function buildMailAccountConfig(Imap $imapServer, Smtp $smtpServer, string $email, string $password): MailAccountConfig {
$imapConfig = new MailServerConfig(
host: $imapServer->getHost(),
port: $imapServer->getPort(),
security: $this->normalizeSslMode($imapServer->getSslMode()),
- username: $response->getEmail(),
- password: $response->getPassword(),
+ username: $email,
+ password: $password,
);
$smtpConfig = new MailServerConfig(
host: $smtpServer->getHost(),
port: $smtpServer->getPort(),
security: $this->normalizeSslMode($smtpServer->getSslMode()),
- username: $response->getEmail(),
- password: $response->getPassword(),
+ username: $email,
+ password: $password,
);
return new MailAccountConfig(
- email: $response->getEmail(),
+ email: $email,
imap: $imapConfig,
smtp: $smtpConfig,
);
}
+ /**
+ * Build configuration from MailAccountResponse (existing account)
+ * Note: MailAccountResponse does not include password for security reasons
+ *
+ * @param MailAccountResponse $response The account response from getFunctionalAccount
+ * @return MailAccountConfig The mail account configuration with empty password
+ */
+ private function buildConfigFromAccountResponse(MailAccountResponse $response): MailAccountConfig {
+ // Password is not available when retrieving existing accounts
+ // It should be retrieved from Nextcloud's credential store separately
+ return $this->buildMailAccountConfig(
+ $response->getServer()->getImap(),
+ $response->getServer()->getSmtp(),
+ $response->getEmail(),
+ ''
+ );
+ }
+
+ /**
+ * Build configuration from MailAccountCreatedResponse (newly created account)
+ *
+ * @param MailAccountCreatedResponse $response The account response from createFunctionalAccount
+ * @return MailAccountConfig The mail account configuration with password
+ */
+ private function buildSuccessResponse(MailAccountCreatedResponse $response): MailAccountConfig {
+ return $this->buildMailAccountConfig(
+ $response->getServer()->getImap(),
+ $response->getServer()->getSmtp(),
+ $response->getEmail(),
+ $response->getPassword()
+ );
+ }
+
/**
* Delete an IONOS email account via API
*
@@ -338,6 +431,66 @@ public function deleteEmailAccount(string $userId): bool {
}
}
+ /**
+ * Reset app password for the IONOS mail account (generates a new password)
+ *
+ * @param string $userId The Nextcloud user ID
+ * @param string $appName The application name for the password
+ * @return string The new password
+ * @throws ServiceException
+ */
+ public function resetAppPassword(string $userId, string $appName): string {
+ $this->logger->debug('Resetting IONOS app password', [
+ 'userId' => $userId,
+ 'appName' => $appName,
+ 'extRef' => $this->configService->getExternalReference(),
+ ]);
+
+ try {
+ $apiInstance = $this->createApiInstance();
+ $result = $apiInstance->setAppPassword(
+ self::BRAND,
+ $this->configService->getExternalReference(),
+ $userId,
+ $appName
+ );
+
+ if (is_string($result)) {
+ $this->logger->info('Successfully reset IONOS app password', [
+ 'userId' => $userId,
+ 'appName' => $appName
+ ]);
+ return $result;
+ }
+
+ $this->logger->error('Failed to reset IONOS app password: Unexpected response type', [
+ 'userId' => $userId,
+ 'appName' => $appName,
+ 'result' => $result
+ ]);
+ throw new ServiceException('Failed to reset IONOS app password', self::HTTP_INTERNAL_SERVER_ERROR);
+ } catch (ServiceException $e) {
+ // Re-throw ServiceException without additional logging
+ throw $e;
+ } catch (ApiException $e) {
+ $this->logger->error('API Exception when calling MailConfigurationAPIApi->setAppPassword', [
+ 'statusCode' => $e->getCode(),
+ 'message' => $e->getMessage(),
+ 'responseBody' => $e->getResponseBody(),
+ 'userId' => $userId,
+ 'appName' => $appName
+ ]);
+ throw new ServiceException('Failed to reset IONOS app password: ' . $e->getMessage(), $e->getCode(), $e);
+ } catch (\Exception $e) {
+ $this->logger->error('Exception when calling MailConfigurationAPIApi->setAppPassword', [
+ 'exception' => $e,
+ 'userId' => $userId,
+ 'appName' => $appName
+ ]);
+ throw new ServiceException('Failed to reset IONOS app password', self::HTTP_INTERNAL_SERVER_ERROR, $e);
+ }
+ }
+
/**
* Get the email address of the IONOS account for a specific user
*
@@ -390,4 +543,13 @@ public function tryDeleteEmailAccount(string $userId): void {
// Don't throw - this is a fire and forget operation
}
}
+
+ /**
+ * Get the configured mail domain for IONOS accounts
+ *
+ * @return string The mail domain (e.g., "example.com")
+ */
+ public function getMailDomain(): string {
+ return $this->configService->getMailDomain();
+ }
}
diff --git a/lib/Service/SetupService.php b/lib/Service/SetupService.php
index 40df87afd9..cc07821f7b 100644
--- a/lib/Service/SetupService.php
+++ b/lib/Service/SetupService.php
@@ -77,7 +77,8 @@ public function createNewAccount(string $accountName,
?string $smtpPassword,
string $uid,
string $authMethod,
- ?int $accountId = null): Account {
+ ?int $accountId = null,
+ bool $skipConnectivityTest = false): Account {
$this->logger->info('Setting up manually configured account');
$newAccount = new MailAccount([
'accountId' => $accountId,
@@ -105,7 +106,7 @@ public function createNewAccount(string $accountName,
$newAccount->setAuthMethod($authMethod);
$account = new Account($newAccount);
- if ($authMethod === 'password' && $imapPassword !== null) {
+ if (!$skipConnectivityTest && $authMethod === 'password' && $imapPassword !== null) {
$this->logger->debug('Connecting to account {account}', ['account' => $newAccount->getEmail()]);
$this->testConnectivity($account);
}
diff --git a/tests/Unit/Command/IonosCreateAccountTest.php b/tests/Unit/Command/IonosCreateAccountTest.php
new file mode 100644
index 0000000000..6cc7786f9b
--- /dev/null
+++ b/tests/Unit/Command/IonosCreateAccountTest.php
@@ -0,0 +1,195 @@
+accountCreationService = $this->createMock(IonosAccountCreationService::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->configService = $this->createMock(IonosConfigService::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->command = new IonosCreateAccount(
+ $this->accountCreationService,
+ $this->userManager,
+ $this->configService,
+ $this->logger
+ );
+ }
+
+ private function createInputMock(string $userId, string $name, string $emailUser): InputInterface&MockObject {
+ $input = $this->createMock(InputInterface::class);
+ $arguments = [
+ 'user-id' => $userId,
+ 'email-user' => $emailUser,
+ ];
+ $options = [
+ 'name' => $name,
+ 'output' => null,
+ ];
+ $input->method('getArgument')
+ ->willReturnCallback(fn ($arg) => $arguments[$arg] ?? null);
+ $input->method('getOption')
+ ->willReturnCallback(fn ($opt) => $options[$opt] ?? null);
+ return $input;
+ }
+
+ private function createOutputMock(): OutputInterface&MockObject {
+ return $this->createMock(OutputInterface::class);
+ }
+
+
+ public function testName(): void {
+ $this->assertSame('mail:ionos:create', $this->command->getName());
+ }
+
+ public function testDescription(): void {
+ $this->assertSame('Creates IONOS mail account and configure it in Nextcloud', $this->command->getDescription());
+ }
+
+ public function testArguments(): void {
+ $definition = $this->command->getDefinition();
+
+ // Check arguments
+ $arguments = $definition->getArguments();
+ $argumentNames = array_map(fn ($arg) => $arg->getName(), $arguments);
+
+ $this->assertCount(2, $arguments);
+ $this->assertContains('user-id', $argumentNames);
+ $this->assertContains('email-user', $argumentNames);
+
+ foreach ($arguments as $arg) {
+ $this->assertTrue($arg->isRequired());
+ }
+
+ // Check options
+ $options = $definition->getOptions();
+ $optionNames = array_map(fn ($opt) => $opt->getName(), $options);
+
+ $this->assertContains('name', $optionNames);
+ $this->assertContains('output', $optionNames);
+
+ // Verify 'name' option is required
+ $nameOption = $definition->getOption('name');
+ $this->assertTrue($nameOption->isValueRequired());
+
+ // Verify 'output' option is optional
+ $outputOption = $definition->getOption('output');
+ $this->assertTrue($outputOption->isValueOptional());
+ }
+
+ public function testInvalidUserId(): void {
+ $userId = 'invalidUser';
+ $input = $this->createInputMock($userId, self::TEST_ACCOUNT_NAME, 'testuser');
+ $output = $this->createOutputMock();
+ $output->expects($this->atLeastOnce())
+ ->method('writeln');
+
+ $this->configService->expects($this->once())
+ ->method('isIonosIntegrationEnabled')
+ ->willReturn(true);
+
+ $this->configService->expects($this->once())
+ ->method('getMailDomain')
+ ->willReturn(self::TEST_MAIL_DOMAIN);
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with($userId)
+ ->willReturn(false);
+
+ $result = $this->command->run($input, $output);
+ $this->assertSame(1, $result);
+ }
+
+ public function testSuccessfulAccountCreation(): void {
+ $accountId = 42;
+
+ $input = $this->createInputMock(self::TEST_USER_ID, self::TEST_ACCOUNT_NAME, self::TEST_EMAIL_USER);
+ $output = $this->createOutputMock();
+
+ $this->configService->expects($this->once())
+ ->method('isIonosIntegrationEnabled')
+ ->willReturn(true);
+
+ $this->configService->expects($this->once())
+ ->method('getMailDomain')
+ ->willReturn(self::TEST_MAIL_DOMAIN);
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::TEST_USER_ID)
+ ->willReturn(true);
+
+ $savedAccount = new MailAccount();
+ $savedAccount->setId($accountId);
+ $savedAccount->setEmail(self::TEST_EMAIL_USER);
+
+ $this->accountCreationService->expects($this->once())
+ ->method('createOrUpdateAccount')
+ ->with(self::TEST_USER_ID, self::TEST_EMAIL_USER, self::TEST_ACCOUNT_NAME)
+ ->willReturn($savedAccount);
+
+ $result = $this->command->run($input, $output);
+ $this->assertSame(0, $result);
+ }
+
+ public function testServiceException(): void {
+ $input = $this->createInputMock(self::TEST_USER_ID, self::TEST_ACCOUNT_NAME, self::TEST_EMAIL_USER);
+ $output = $this->createOutputMock();
+
+ $this->configService->expects($this->once())
+ ->method('isIonosIntegrationEnabled')
+ ->willReturn(true);
+
+ $this->configService->expects($this->once())
+ ->method('getMailDomain')
+ ->willReturn(self::TEST_MAIL_DOMAIN);
+
+ $this->userManager->expects($this->once())
+ ->method('userExists')
+ ->with(self::TEST_USER_ID)
+ ->willReturn(true);
+
+ $exception = new ServiceException('IONOS API error', 500);
+ $this->accountCreationService->expects($this->once())
+ ->method('createOrUpdateAccount')
+ ->with(self::TEST_USER_ID, self::TEST_EMAIL_USER, self::TEST_ACCOUNT_NAME)
+ ->willThrowException($exception);
+
+ $result = $this->command->run($input, $output);
+ $this->assertSame(1, $result);
+ }
+}
diff --git a/tests/Unit/Controller/AccountsControllerTest.php b/tests/Unit/Controller/AccountsControllerTest.php
index c37af7da55..4f311bc1c7 100644
--- a/tests/Unit/Controller/AccountsControllerTest.php
+++ b/tests/Unit/Controller/AccountsControllerTest.php
@@ -236,7 +236,7 @@ public function testCreateManualSuccess(): void {
$account = $this->createMock(Account::class);
$this->setupService->expects(self::once())
->method('createNewAccount')
- ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId, 'password')
+ ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId, 'password', null, false)
->willReturn($account);
$response = $this->controller->create($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword);
@@ -246,6 +246,35 @@ public function testCreateManualSuccess(): void {
self::assertEquals($expectedResponse, $response);
}
+ public function testCreateManualSuccessWithSkipConnectivityTest(): void {
+ $this->config->expects(self::once())
+ ->method('getAppValue')
+ ->willReturn('yes');
+ $email = 'user@domain.tld';
+ $accountName = 'Mail';
+ $imapHost = 'localhost';
+ $imapPort = 993;
+ $imapSslMode = 'ssl';
+ $imapUser = 'user@domain.tld';
+ $imapPassword = 'mypassword';
+ $smtpHost = 'localhost';
+ $smtpPort = 465;
+ $smtpSslMode = 'none';
+ $smtpUser = 'user@domain.tld';
+ $smtpPassword = 'mypassword';
+ $account = $this->createMock(Account::class);
+ $this->setupService->expects(self::once())
+ ->method('createNewAccount')
+ ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId, 'password', null, true)
+ ->willReturn($account);
+
+ $response = $this->controller->create($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, 'password', true);
+
+ $expectedResponse = \OCA\Mail\Http\JsonResponse::success($account, Http::STATUS_CREATED);
+
+ self::assertEquals($expectedResponse, $response);
+ }
+
public function testCreateManualNotAllowed(): void {
$email = 'user@domain.tld';
$accountName = 'Mail';
@@ -289,7 +318,7 @@ public function testCreateManualFailure(): void {
$smtpPassword = 'mypassword';
$this->setupService->expects(self::once())
->method('createNewAccount')
- ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId, 'password')
+ ->with($accountName, $email, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->userId, 'password', null, false)
->willThrowException(new ClientException());
$this->expectException(ClientException::class);
diff --git a/tests/Unit/Controller/IonosAccountsControllerTest.php b/tests/Unit/Controller/IonosAccountsControllerTest.php
index ad43b0085d..06b8b4bb50 100644
--- a/tests/Unit/Controller/IonosAccountsControllerTest.php
+++ b/tests/Unit/Controller/IonosAccountsControllerTest.php
@@ -10,26 +10,26 @@
namespace OCA\Mail\Tests\Unit\Controller;
use ChristophWurst\Nextcloud\Testing\TestCase;
-use OCA\Mail\Controller\AccountsController;
use OCA\Mail\Controller\IonosAccountsController;
+use OCA\Mail\Db\MailAccount;
use OCA\Mail\Exception\ServiceException;
-use OCA\Mail\Service\IONOS\Dto\MailAccountConfig;
-use OCA\Mail\Service\IONOS\Dto\MailServerConfig;
-use OCA\Mail\Service\IONOS\IonosMailService;
+use OCA\Mail\Service\IONOS\IonosAccountCreationService;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
-use ReflectionClass;
class IonosAccountsControllerTest extends TestCase {
private string $appName;
private IRequest&MockObject $request;
- private IonosMailService&MockObject $ionosMailService;
+ private IonosAccountCreationService&MockObject $accountCreationService;
- private AccountsController&MockObject $accountsController;
+
+ private IUserSession&MockObject $userSession;
private LoggerInterface|MockObject $logger;
@@ -40,19 +40,28 @@ protected function setUp(): void {
$this->appName = 'mail';
$this->request = $this->createMock(IRequest::class);
- $this->ionosMailService = $this->createMock(IonosMailService::class);
- $this->accountsController = $this->createMock(AccountsController::class);
+ $this->accountCreationService = $this->createMock(IonosAccountCreationService::class);
+ $this->userSession = $this->createMock(IUserSession::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->controller = new IonosAccountsController(
$this->appName,
$this->request,
- $this->ionosMailService,
- $this->accountsController,
+ $this->accountCreationService,
+ $this->userSession,
$this->logger,
);
}
+ /**
+ * Helper method to setup user session mock
+ */
+ private function setupUserSession(string $userId): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn($userId);
+ $this->userSession->method('getUser')->willReturn($user);
+ }
+
public function testCreateWithMissingFields(): void {
// Test with empty account name
$response = $this->controller->create('', 'testuser');
@@ -75,87 +84,63 @@ public function testCreateSuccess(): void {
$accountName = 'Test Account';
$emailUser = 'test';
$emailAddress = 'test@example.com';
+ $userId = 'testuser';
- // Create MailAccountConfig DTO
- $imapConfig = new MailServerConfig(
- host: 'mail.localhost',
- port: 1143,
- security: 'none',
- username: $emailAddress,
- password: 'tmp',
- );
-
- $smtpConfig = new MailServerConfig(
- host: 'mail.localhost',
- port: 1587,
- security: 'none',
- username: $emailAddress,
- password: 'tmp',
- );
-
- $mailAccountConfig = new MailAccountConfig(
- email: $emailAddress,
- imap: $imapConfig,
- smtp: $smtpConfig,
- );
+ // Setup user session
+ $this->setupUserSession($userId);
- // Mock successful IONOS mail service response
- $this->ionosMailService->method('createEmailAccount')
- ->with($emailUser)
- ->willReturn($mailAccountConfig);
+ // Create a real MailAccount instance
+ $mailAccount = new MailAccount();
+ $mailAccount->setId(1);
+ $mailAccount->setUserId($userId);
+ $mailAccount->setName($accountName);
+ $mailAccount->setEmail($emailAddress);
- // Mock account creation response
- $accountData = ['id' => 1, 'emailAddress' => $emailAddress];
- $accountResponse = $this->createMock(JSONResponse::class);
- $accountResponse->method('getData')->willReturn($accountData);
-
- $this->accountsController
- ->method('create')
- ->with(
- $accountName,
- $emailAddress,
- 'mail.localhost',
- 1143,
- 'none',
- $emailAddress,
- 'tmp',
- 'mail.localhost',
- 1587,
- 'none',
- $emailAddress,
- 'tmp',
- )
- ->willReturn($accountResponse);
+ // Mock account creation service to return a successful account
+ $this->accountCreationService->expects($this->once())
+ ->method('createOrUpdateAccount')
+ ->with($userId, $emailUser, $accountName)
+ ->willReturn($mailAccount);
$response = $this->controller->create($accountName, $emailUser);
- // The controller now directly returns the AccountsController response
- $this->assertSame($accountResponse, $response);
+ $this->assertInstanceOf(JSONResponse::class, $response);
+ $data = $response->getData();
+ $this->assertEquals(1, $data['id']);
+ $this->assertEquals($accountName, $data['accountName']);
+ $this->assertEquals($emailAddress, $data['emailAddress']);
}
public function testCreateWithServiceException(): void {
$accountName = 'Test Account';
$emailUser = 'test';
+ $userId = 'testuser';
+
+ // Setup user session
+ $this->setupUserSession($userId);
- // Mock IONOS mail service to throw ServiceException
- $this->ionosMailService->method('createEmailAccount')
- ->with($emailUser)
+ // Mock account creation service to throw ServiceException
+ $this->accountCreationService->expects($this->once())
+ ->method('createOrUpdateAccount')
+ ->with($userId, $emailUser, $accountName)
->willThrowException(new ServiceException('Failed to create email account'));
$this->logger
->expects($this->once())
->method('error')
->with(
- 'IONOS service error: Failed to create email account',
+ 'IONOS service error during account creation: Failed to create email account',
[
'error' => 'IONOS_API_ERROR',
'statusCode' => 0,
+ 'message' => 'Failed to create email account',
]
);
$expectedResponse = \OCA\Mail\Http\JsonResponse::fail([
'error' => 'IONOS_API_ERROR',
'statusCode' => 0,
+ 'message' => 'Failed to create email account',
]);
$response = $this->controller->create($accountName, $emailUser);
@@ -165,26 +150,34 @@ public function testCreateWithServiceException(): void {
public function testCreateWithServiceExceptionWithStatusCode(): void {
$accountName = 'Test Account';
$emailUser = 'test';
+ $userId = 'testuser';
- // Mock IONOS mail service to throw ServiceException with HTTP 409 (Duplicate)
- $this->ionosMailService->method('createEmailAccount')
- ->with($emailUser)
+ // Setup user session
+ $this->setupUserSession($userId);
+
+ // Mock account creation service to throw ServiceException with status code
+ $this->accountCreationService->expects($this->once())
+ ->method('createOrUpdateAccount')
+ ->with($userId, $emailUser, $accountName)
->willThrowException(new ServiceException('Duplicate email account', 409));
+
$this->logger
->expects($this->once())
->method('error')
->with(
- 'IONOS service error: Duplicate email account',
+ 'IONOS service error during account creation: Duplicate email account',
[
'error' => 'IONOS_API_ERROR',
'statusCode' => 409,
+ 'message' => 'Duplicate email account',
]
);
$expectedResponse = \OCA\Mail\Http\JsonResponse::fail([
'error' => 'IONOS_API_ERROR',
'statusCode' => 409,
+ 'message' => 'Duplicate email account',
]);
$response = $this->controller->create($accountName, $emailUser);
@@ -194,10 +187,15 @@ public function testCreateWithServiceExceptionWithStatusCode(): void {
public function testCreateWithGenericException(): void {
$accountName = 'Test Account';
$emailUser = 'test';
+ $userId = 'testuser';
+
+ // Setup user session
+ $this->setupUserSession($userId);
- // Mock IONOS mail service to throw a generic exception
- $this->ionosMailService->method('createEmailAccount')
- ->with($emailUser)
+ // Mock account creation service to throw a generic exception
+ $this->accountCreationService->expects($this->once())
+ ->method('createOrUpdateAccount')
+ ->with($userId, $emailUser, $accountName)
->willThrowException(new \Exception('Generic error'));
$expectedResponse = \OCA\Mail\Http\JsonResponse::error('Could not create account',
@@ -209,61 +207,4 @@ public function testCreateWithGenericException(): void {
self::assertEquals($expectedResponse, $response);
}
-
-
- public function testCreateNextcloudMailAccount(): void {
- $accountName = 'Test Account';
- $emailAddress = 'test@example.com';
-
- $imapConfig = new MailServerConfig(
- host: 'mail.localhost',
- port: 1143,
- security: 'none',
- username: $emailAddress,
- password: 'tmp',
- );
-
- $smtpConfig = new MailServerConfig(
- host: 'mail.localhost',
- port: 1587,
- security: 'none',
- username: $emailAddress,
- password: 'tmp',
- );
-
- $mailConfig = new MailAccountConfig(
- email: $emailAddress,
- imap: $imapConfig,
- smtp: $smtpConfig,
- );
-
- $expectedResponse = $this->createMock(JSONResponse::class);
-
- $this->accountsController
- ->expects($this->once())
- ->method('create')
- ->with(
- $accountName,
- $emailAddress,
- 'mail.localhost',
- 1143,
- 'none',
- $emailAddress,
- 'tmp',
- 'mail.localhost',
- 1587,
- 'none',
- $emailAddress,
- 'tmp',
- )
- ->willReturn($expectedResponse);
-
- $reflection = new ReflectionClass($this->controller);
- $method = $reflection->getMethod('createNextcloudMailAccount');
- $method->setAccessible(true);
-
- $result = $method->invoke($this->controller, $accountName, $mailConfig);
-
- $this->assertSame($expectedResponse, $result);
- }
}
diff --git a/tests/Unit/Service/IONOS/ConflictResolutionResultTest.php b/tests/Unit/Service/IONOS/ConflictResolutionResultTest.php
new file mode 100644
index 0000000000..a140e4bca1
--- /dev/null
+++ b/tests/Unit/Service/IONOS/ConflictResolutionResultTest.php
@@ -0,0 +1,158 @@
+accountConfig = new MailAccountConfig(
+ email: 'user@example.com',
+ imap: $imapConfig,
+ smtp: $smtpConfig,
+ );
+ }
+
+ public function testRetryFactoryMethod(): void {
+ $result = ConflictResolutionResult::retry($this->accountConfig);
+
+ $this->assertInstanceOf(ConflictResolutionResult::class, $result);
+ $this->assertTrue($result->canRetry());
+ $this->assertInstanceOf(MailAccountConfig::class, $result->getAccountConfig());
+ $this->assertSame($this->accountConfig, $result->getAccountConfig());
+ $this->assertFalse($result->hasEmailMismatch());
+ $this->assertNull($result->getExpectedEmail());
+ $this->assertNull($result->getExistingEmail());
+ }
+
+ public function testNoExistingAccountFactoryMethod(): void {
+ $result = ConflictResolutionResult::noExistingAccount();
+
+ $this->assertInstanceOf(ConflictResolutionResult::class, $result);
+ $this->assertFalse($result->canRetry());
+ $this->assertNull($result->getAccountConfig());
+ $this->assertFalse($result->hasEmailMismatch());
+ $this->assertNull($result->getExpectedEmail());
+ $this->assertNull($result->getExistingEmail());
+ }
+
+ public function testEmailMismatchFactoryMethod(): void {
+ $expectedEmail = 'expected@example.com';
+ $existingEmail = 'existing@example.com';
+
+ $result = ConflictResolutionResult::emailMismatch($expectedEmail, $existingEmail);
+
+ $this->assertInstanceOf(ConflictResolutionResult::class, $result);
+ $this->assertFalse($result->canRetry());
+ $this->assertNull($result->getAccountConfig());
+ $this->assertTrue($result->hasEmailMismatch());
+ $this->assertEquals($expectedEmail, $result->getExpectedEmail());
+ $this->assertEquals($existingEmail, $result->getExistingEmail());
+ }
+
+ public function testRetryResultHasCorrectState(): void {
+ $result = ConflictResolutionResult::retry($this->accountConfig);
+
+ // Verify all state is correct for retry scenario
+ $this->assertTrue($result->canRetry(), 'Should be able to retry');
+ $this->assertNotNull($result->getAccountConfig(), 'Should have account config');
+ $this->assertEquals('user@example.com', $result->getAccountConfig()->getEmail());
+ }
+
+ public function testNoExistingAccountResultHasCorrectState(): void {
+ $result = ConflictResolutionResult::noExistingAccount();
+
+ // Verify all state is correct for no existing account scenario
+ $this->assertFalse($result->canRetry(), 'Should not be able to retry');
+ $this->assertNull($result->getAccountConfig(), 'Should not have account config');
+ $this->assertFalse($result->hasEmailMismatch(), 'Should not have email mismatch');
+ }
+
+ public function testEmailMismatchResultHasCorrectState(): void {
+ $result = ConflictResolutionResult::emailMismatch('user1@example.com', 'user2@example.com');
+
+ // Verify all state is correct for email mismatch scenario
+ $this->assertFalse($result->canRetry(), 'Should not be able to retry');
+ $this->assertNull($result->getAccountConfig(), 'Should not have account config');
+ $this->assertTrue($result->hasEmailMismatch(), 'Should have email mismatch');
+ $this->assertNotNull($result->getExpectedEmail(), 'Should have expected email');
+ $this->assertNotNull($result->getExistingEmail(), 'Should have existing email');
+ }
+
+ public function testEmailMismatchWithSameEmail(): void {
+ // Even with same email, if using emailMismatch() factory, it should still mark as mismatch
+ $email = 'same@example.com';
+ $result = ConflictResolutionResult::emailMismatch($email, $email);
+
+ $this->assertTrue($result->hasEmailMismatch());
+ $this->assertEquals($email, $result->getExpectedEmail());
+ $this->assertEquals($email, $result->getExistingEmail());
+ }
+
+ public function testRetryResultPreservesAccountConfigData(): void {
+ $result = ConflictResolutionResult::retry($this->accountConfig);
+ $retrievedConfig = $result->getAccountConfig();
+
+ $this->assertNotNull($retrievedConfig);
+ $this->assertEquals('user@example.com', $retrievedConfig->getEmail());
+ $this->assertEquals('imap.example.com', $retrievedConfig->getImap()->getHost());
+ $this->assertEquals('smtp.example.com', $retrievedConfig->getSmtp()->getHost());
+ }
+
+ public function testEmailMismatchWithEmptyStrings(): void {
+ $result = ConflictResolutionResult::emailMismatch('', '');
+
+ $this->assertTrue($result->hasEmailMismatch());
+ $this->assertEquals('', $result->getExpectedEmail());
+ $this->assertEquals('', $result->getExistingEmail());
+ }
+
+ public function testMultipleInstancesAreIndependent(): void {
+ $result1 = ConflictResolutionResult::retry($this->accountConfig);
+ $result2 = ConflictResolutionResult::noExistingAccount();
+ $result3 = ConflictResolutionResult::emailMismatch('a@test.com', 'b@test.com');
+
+ // Each instance should maintain its own state
+ $this->assertTrue($result1->canRetry());
+ $this->assertFalse($result2->canRetry());
+ $this->assertFalse($result3->canRetry());
+
+ $this->assertNotNull($result1->getAccountConfig());
+ $this->assertNull($result2->getAccountConfig());
+ $this->assertNull($result3->getAccountConfig());
+
+ $this->assertFalse($result1->hasEmailMismatch());
+ $this->assertFalse($result2->hasEmailMismatch());
+ $this->assertTrue($result3->hasEmailMismatch());
+ }
+}
diff --git a/tests/Unit/Service/IONOS/Dto/MailAccountConfigTest.php b/tests/Unit/Service/IONOS/Dto/MailAccountConfigTest.php
index 377e7c59ff..5b7418a70d 100644
--- a/tests/Unit/Service/IONOS/Dto/MailAccountConfigTest.php
+++ b/tests/Unit/Service/IONOS/Dto/MailAccountConfigTest.php
@@ -245,4 +245,30 @@ public function testNestedObjectAccess(): void {
$this->assertEquals('imap.example.com', $imapHost);
$this->assertEquals('smtp.example.com', $smtpHost);
}
+
+ public function testWithPassword(): void {
+ $newPassword = 'new-secure-password';
+
+ // Create a new config with updated password
+ $updatedConfig = $this->accountConfig->withPassword($newPassword);
+
+ // Original config should remain unchanged (immutable)
+ $this->assertEquals('imap-password', $this->accountConfig->getImap()->getPassword());
+ $this->assertEquals('smtp-password', $this->accountConfig->getSmtp()->getPassword());
+
+ // New config should have the new password for both IMAP and SMTP
+ $this->assertEquals($newPassword, $updatedConfig->getImap()->getPassword());
+ $this->assertEquals($newPassword, $updatedConfig->getSmtp()->getPassword());
+
+ // Other properties should remain the same
+ $this->assertEquals($this->accountConfig->getEmail(), $updatedConfig->getEmail());
+ $this->assertEquals($this->accountConfig->getImap()->getHost(), $updatedConfig->getImap()->getHost());
+ $this->assertEquals($this->accountConfig->getImap()->getPort(), $updatedConfig->getImap()->getPort());
+ $this->assertEquals($this->accountConfig->getImap()->getSecurity(), $updatedConfig->getImap()->getSecurity());
+ $this->assertEquals($this->accountConfig->getImap()->getUsername(), $updatedConfig->getImap()->getUsername());
+ $this->assertEquals($this->accountConfig->getSmtp()->getHost(), $updatedConfig->getSmtp()->getHost());
+ $this->assertEquals($this->accountConfig->getSmtp()->getPort(), $updatedConfig->getSmtp()->getPort());
+ $this->assertEquals($this->accountConfig->getSmtp()->getSecurity(), $updatedConfig->getSmtp()->getSecurity());
+ $this->assertEquals($this->accountConfig->getSmtp()->getUsername(), $updatedConfig->getSmtp()->getUsername());
+ }
}
diff --git a/tests/Unit/Service/IONOS/Dto/MailServerConfigTest.php b/tests/Unit/Service/IONOS/Dto/MailServerConfigTest.php
index 9c45508352..a5a9b6625b 100644
--- a/tests/Unit/Service/IONOS/Dto/MailServerConfigTest.php
+++ b/tests/Unit/Service/IONOS/Dto/MailServerConfigTest.php
@@ -165,4 +165,23 @@ public function testDifferentSecurityTypes(): void {
$this->assertEquals('tls', $tlsConfig->getSecurity());
$this->assertEquals('none', $noneConfig->getSecurity());
}
+
+ public function testWithPassword(): void {
+ $newPassword = 'new-secure-password';
+
+ // Create a new config with updated password
+ $updatedConfig = $this->config->withPassword($newPassword);
+
+ // Original config should remain unchanged (immutable)
+ $this->assertEquals('secret123', $this->config->getPassword());
+
+ // New config should have the new password
+ $this->assertEquals($newPassword, $updatedConfig->getPassword());
+
+ // Other properties should remain the same
+ $this->assertEquals($this->config->getHost(), $updatedConfig->getHost());
+ $this->assertEquals($this->config->getPort(), $updatedConfig->getPort());
+ $this->assertEquals($this->config->getSecurity(), $updatedConfig->getSecurity());
+ $this->assertEquals($this->config->getUsername(), $updatedConfig->getUsername());
+ }
}
diff --git a/tests/Unit/Service/IONOS/IonosAccountConflictResolverTest.php b/tests/Unit/Service/IONOS/IonosAccountConflictResolverTest.php
new file mode 100644
index 0000000000..5c8379f2b8
--- /dev/null
+++ b/tests/Unit/Service/IONOS/IonosAccountConflictResolverTest.php
@@ -0,0 +1,182 @@
+ionosMailService = $this->createMock(IonosMailService::class);
+ $this->ionosConfigService = $this->createMock(IonosConfigService::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->resolver = new IonosAccountConflictResolver(
+ $this->ionosMailService,
+ $this->ionosConfigService,
+ $this->logger,
+ );
+ }
+
+ public function testResolveConflictWithNoExistingAccount(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+
+ $this->ionosMailService->method('getAccountConfigForUser')
+ ->with($userId)
+ ->willReturn(null);
+
+ $this->logger
+ ->expects($this->once())
+ ->method('debug')
+ ->with('No existing IONOS account found for conflict resolution', ['userId' => $userId]);
+
+ $result = $this->resolver->resolveConflict($userId, $emailUser);
+
+ $this->assertFalse($result->canRetry());
+ $this->assertNull($result->getAccountConfig());
+ $this->assertFalse($result->hasEmailMismatch());
+ }
+
+ public function testResolveConflictWithMatchingEmail(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $domain = 'example.com';
+ $emailAddress = 'test@example.com';
+ $newPassword = 'new-app-password-123';
+
+ // Create MailAccountConfig DTO without password (as API returns)
+ $imapConfig = new MailServerConfig(
+ host: 'mail.localhost',
+ port: 1143,
+ security: 'none',
+ username: $emailAddress,
+ password: '', // Empty password from getAccountConfigForUser
+ );
+
+ $smtpConfig = new MailServerConfig(
+ host: 'mail.localhost',
+ port: 1587,
+ security: 'none',
+ username: $emailAddress,
+ password: '', // Empty password from getAccountConfigForUser
+ );
+
+ $mailAccountConfig = new MailAccountConfig(
+ email: $emailAddress,
+ imap: $imapConfig,
+ smtp: $smtpConfig,
+ );
+
+ $this->ionosMailService->method('getAccountConfigForUser')
+ ->with($userId)
+ ->willReturn($mailAccountConfig);
+
+ $this->ionosConfigService->method('getMailDomain')
+ ->willReturn($domain);
+
+ // Expect resetAppPassword to be called
+ $this->ionosMailService
+ ->expects($this->once())
+ ->method('resetAppPassword')
+ ->with($userId, 'NEXTCLOUD_WORKSPACE')
+ ->willReturn($newPassword);
+
+ $this->logger
+ ->expects($this->once())
+ ->method('info')
+ ->with(
+ 'IONOS account already exists, retrieving new password for retry',
+ ['emailAddress' => $emailAddress, 'userId' => $userId]
+ );
+
+ $result = $this->resolver->resolveConflict($userId, $emailUser);
+
+ $this->assertTrue($result->canRetry());
+ $this->assertNotNull($result->getAccountConfig());
+ $this->assertFalse($result->hasEmailMismatch());
+
+ // Verify the returned config has the new password
+ $resultConfig = $result->getAccountConfig();
+ $this->assertEquals($newPassword, $resultConfig->getImap()->getPassword());
+ $this->assertEquals($newPassword, $resultConfig->getSmtp()->getPassword());
+ }
+
+ public function testResolveConflictWithEmailMismatch(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $domain = 'example.com';
+ $expectedEmail = 'test@example.com';
+ $existingEmail = 'different@example.com';
+
+ // Create MailAccountConfig DTO with different email
+ $imapConfig = new MailServerConfig(
+ host: 'mail.localhost',
+ port: 1143,
+ security: 'none',
+ username: $existingEmail,
+ password: 'tmp',
+ );
+
+ $smtpConfig = new MailServerConfig(
+ host: 'mail.localhost',
+ port: 1587,
+ security: 'none',
+ username: $existingEmail,
+ password: 'tmp',
+ );
+
+ $mailAccountConfig = new MailAccountConfig(
+ email: $existingEmail,
+ imap: $imapConfig,
+ smtp: $smtpConfig,
+ );
+
+ $this->ionosMailService->method('getAccountConfigForUser')
+ ->with($userId)
+ ->willReturn($mailAccountConfig);
+
+ $this->ionosConfigService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->logger
+ ->expects($this->once())
+ ->method('warning')
+ ->with(
+ 'IONOS account exists but email mismatch',
+ ['requestedEmail' => $expectedEmail, 'existingEmail' => $existingEmail, 'userId' => $userId]
+ );
+
+ $result = $this->resolver->resolveConflict($userId, $emailUser);
+
+ $this->assertFalse($result->canRetry());
+ $this->assertNull($result->getAccountConfig());
+ $this->assertTrue($result->hasEmailMismatch());
+ $this->assertEquals($expectedEmail, $result->getExpectedEmail());
+ $this->assertEquals($existingEmail, $result->getExistingEmail());
+ }
+}
diff --git a/tests/Unit/Service/IONOS/IonosAccountCreationServiceTest.php b/tests/Unit/Service/IONOS/IonosAccountCreationServiceTest.php
new file mode 100644
index 0000000000..9e37888dda
--- /dev/null
+++ b/tests/Unit/Service/IONOS/IonosAccountCreationServiceTest.php
@@ -0,0 +1,475 @@
+ionosMailService = $this->createMock(IonosMailService::class);
+ $this->conflictResolver = $this->createMock(IonosAccountConflictResolver::class);
+ $this->accountService = $this->createMock(AccountService::class);
+ $this->crypto = $this->createMock(ICrypto::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->service = new IonosAccountCreationService(
+ $this->ionosMailService,
+ $this->conflictResolver,
+ $this->accountService,
+ $this->crypto,
+ $this->logger,
+ );
+ }
+
+ public function testCreateOrUpdateAccountNewAccount(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $accountName = 'Test Account';
+ $domain = 'example.com';
+ $emailAddress = 'test@example.com';
+ $password = 'test-password-123';
+
+ $mailConfig = $this->createMailAccountConfig($emailAddress, $password);
+
+ $this->ionosMailService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with($userId, $emailAddress)
+ ->willReturn([]);
+
+ $this->ionosMailService->expects($this->once())
+ ->method('createEmailAccountForUser')
+ ->with($userId, $emailUser)
+ ->willReturn($mailConfig);
+
+ $this->crypto->expects($this->exactly(2))
+ ->method('encrypt')
+ ->with($password)
+ ->willReturn('encrypted-' . $password);
+
+ $savedAccount = new MailAccount();
+ $savedAccount->setId(1);
+ $savedAccount->setUserId($userId);
+ $savedAccount->setEmail($emailAddress);
+
+ $this->accountService->expects($this->once())
+ ->method('save')
+ ->willReturnCallback(function (MailAccount $account) use ($savedAccount) {
+ $this->assertEquals('testuser', $account->getUserId());
+ $this->assertEquals('Test Account', $account->getName());
+ $this->assertEquals('test@example.com', $account->getEmail());
+ $this->assertEquals('password', $account->getAuthMethod());
+ return $savedAccount;
+ });
+
+ $this->logger->expects($this->exactly(3))
+ ->method('info');
+
+ $result = $this->service->createOrUpdateAccount($userId, $emailUser, $accountName);
+
+ $this->assertInstanceOf(MailAccount::class, $result);
+ $this->assertEquals(1, $result->getId());
+ }
+
+ public function testCreateOrUpdateAccountExistingNextcloudAccountSuccess(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $accountName = 'Test Account';
+ $domain = 'example.com';
+ $emailAddress = 'test@example.com';
+ $newPassword = 'new-password-456';
+
+ $existingAccount = new MailAccount();
+ $existingAccount->setId(5);
+ $existingAccount->setUserId($userId);
+ $existingAccount->setEmail($emailAddress);
+
+ $mailConfig = $this->createMailAccountConfig($emailAddress, $newPassword);
+
+ $resolutionResult = ConflictResolutionResult::retry($mailConfig);
+
+ $this->ionosMailService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with($userId, $emailAddress)
+ ->willReturn([$this->createAccountWithMailAccount($existingAccount)]);
+
+ $this->conflictResolver->expects($this->once())
+ ->method('resolveConflict')
+ ->with($userId, $emailUser)
+ ->willReturn($resolutionResult);
+
+ $this->crypto->expects($this->exactly(2))
+ ->method('encrypt')
+ ->with($newPassword)
+ ->willReturn('encrypted-' . $newPassword);
+
+ $this->accountService->expects($this->once())
+ ->method('update')
+ ->with($existingAccount)
+ ->willReturn($existingAccount);
+
+ $this->logger->expects($this->exactly(2))
+ ->method('info');
+
+ $result = $this->service->createOrUpdateAccount($userId, $emailUser, $accountName);
+
+ $this->assertInstanceOf(MailAccount::class, $result);
+ $this->assertEquals(5, $result->getId());
+ }
+
+ public function testCreateOrUpdateAccountExistingAccountEmailMismatch(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $accountName = 'Test Account';
+ $domain = 'example.com';
+ $emailAddress = 'test@example.com';
+ $existingEmail = 'different@example.com';
+
+ $existingAccount = new MailAccount();
+ $existingAccount->setId(5);
+ $existingAccount->setUserId($userId);
+ $existingAccount->setEmail($emailAddress);
+
+ $resolutionResult = ConflictResolutionResult::emailMismatch($emailAddress, $existingEmail);
+
+ $this->ionosMailService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with($userId, $emailAddress)
+ ->willReturn([$this->createAccountWithMailAccount($existingAccount)]);
+
+ $this->conflictResolver->expects($this->once())
+ ->method('resolveConflict')
+ ->with($userId, $emailUser)
+ ->willReturn($resolutionResult);
+
+ $this->logger->expects($this->once())
+ ->method('info')
+ ->with('Nextcloud mail account already exists, resetting credentials', $this->anything());
+
+ $this->expectException(ServiceException::class);
+ $this->expectExceptionCode(409);
+ $this->expectExceptionMessage('IONOS account exists but email mismatch');
+
+ $this->service->createOrUpdateAccount($userId, $emailUser, $accountName);
+ }
+
+ public function testCreateOrUpdateAccountExistingAccountNoIonosAccount(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $accountName = 'Test Account';
+ $domain = 'example.com';
+ $emailAddress = 'test@example.com';
+
+ $existingAccount = new MailAccount();
+ $existingAccount->setId(5);
+ $existingAccount->setUserId($userId);
+ $existingAccount->setEmail($emailAddress);
+
+ $resolutionResult = ConflictResolutionResult::noExistingAccount();
+
+ $this->ionosMailService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with($userId, $emailAddress)
+ ->willReturn([$this->createAccountWithMailAccount($existingAccount)]);
+
+ $this->conflictResolver->expects($this->once())
+ ->method('resolveConflict')
+ ->with($userId, $emailUser)
+ ->willReturn($resolutionResult);
+
+ $this->logger->expects($this->once())
+ ->method('info')
+ ->with('Nextcloud mail account already exists, resetting credentials', $this->anything());
+
+ $this->expectException(ServiceException::class);
+ $this->expectExceptionCode(500);
+ $this->expectExceptionMessage('Nextcloud account exists but no IONOS account found');
+
+ $this->service->createOrUpdateAccount($userId, $emailUser, $accountName);
+ }
+
+ public function testCreateOrUpdateAccountNewAccountWithConflictResolution(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $accountName = 'Test Account';
+ $domain = 'example.com';
+ $emailAddress = 'test@example.com';
+ $password = 'reset-password-789';
+
+ $mailConfig = $this->createMailAccountConfig($emailAddress, $password);
+
+ $resolutionResult = ConflictResolutionResult::retry($mailConfig);
+
+ $this->ionosMailService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with($userId, $emailAddress)
+ ->willReturn([]);
+
+ // First attempt to create fails
+ $this->ionosMailService->expects($this->once())
+ ->method('createEmailAccountForUser')
+ ->with($userId, $emailUser)
+ ->willThrowException(new ServiceException('Account already exists', 409));
+
+ // Conflict resolution succeeds
+ $this->conflictResolver->expects($this->once())
+ ->method('resolveConflict')
+ ->with($userId, $emailUser)
+ ->willReturn($resolutionResult);
+
+ $this->crypto->expects($this->exactly(2))
+ ->method('encrypt')
+ ->with($password)
+ ->willReturn('encrypted-' . $password);
+
+ $savedAccount = new MailAccount();
+ $savedAccount->setId(2);
+ $savedAccount->setUserId($userId);
+ $savedAccount->setEmail($emailAddress);
+
+ $this->accountService->expects($this->once())
+ ->method('save')
+ ->willReturn($savedAccount);
+
+ $this->logger->expects($this->exactly(3))
+ ->method('info');
+
+ $result = $this->service->createOrUpdateAccount($userId, $emailUser, $accountName);
+
+ $this->assertInstanceOf(MailAccount::class, $result);
+ $this->assertEquals(2, $result->getId());
+ }
+
+ public function testCreateOrUpdateAccountNewAccountConflictResolutionFails(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $accountName = 'Test Account';
+ $domain = 'example.com';
+ $emailAddress = 'test@example.com';
+
+ $resolutionResult = ConflictResolutionResult::noExistingAccount();
+
+ $this->ionosMailService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with($userId, $emailAddress)
+ ->willReturn([]);
+
+ $originalException = new ServiceException('Creation failed', 500);
+
+ $this->ionosMailService->expects($this->once())
+ ->method('createEmailAccountForUser')
+ ->with($userId, $emailUser)
+ ->willThrowException($originalException);
+
+ $this->conflictResolver->expects($this->once())
+ ->method('resolveConflict')
+ ->with($userId, $emailUser)
+ ->willReturn($resolutionResult);
+
+ $this->logger->expects($this->exactly(2))
+ ->method('info');
+
+ $this->expectException(ServiceException::class);
+ $this->expectExceptionMessage('Creation failed');
+
+ $this->service->createOrUpdateAccount($userId, $emailUser, $accountName);
+ }
+
+ public function testCreateOrUpdateAccountNewAccountConflictResolutionEmailMismatch(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $accountName = 'Test Account';
+ $domain = 'example.com';
+ $expectedEmail = 'test@example.com';
+ $existingEmail = 'other@example.com';
+
+ $resolutionResult = ConflictResolutionResult::emailMismatch($expectedEmail, $existingEmail);
+
+ $this->ionosMailService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with($userId, $expectedEmail)
+ ->willReturn([]);
+
+ $originalException = new ServiceException('Account already exists', 409);
+
+ $this->ionosMailService->expects($this->once())
+ ->method('createEmailAccountForUser')
+ ->with($userId, $emailUser)
+ ->willThrowException($originalException);
+
+ $this->conflictResolver->expects($this->once())
+ ->method('resolveConflict')
+ ->with($userId, $emailUser)
+ ->willReturn($resolutionResult);
+
+ $this->logger->expects($this->exactly(2))
+ ->method('info');
+
+ $this->expectException(ServiceException::class);
+ $this->expectExceptionCode(409);
+ $this->expectExceptionMessage('IONOS account exists but email mismatch');
+
+ $this->service->createOrUpdateAccount($userId, $emailUser, $accountName);
+ }
+
+ public function testCreateOrUpdateAccountSetsCorrectCredentials(): void {
+ $userId = 'testuser';
+ $emailUser = 'test';
+ $accountName = 'Test Account';
+ $domain = 'example.com';
+ $emailAddress = 'test@example.com';
+ $password = 'secret-password';
+
+ $mailConfig = $this->createMailAccountConfig($emailAddress, $password);
+
+ $this->ionosMailService->method('getMailDomain')
+ ->willReturn($domain);
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with($userId, $emailAddress)
+ ->willReturn([]);
+
+ $this->ionosMailService->expects($this->once())
+ ->method('createEmailAccountForUser')
+ ->with($userId, $emailUser)
+ ->willReturn($mailConfig);
+
+ $this->crypto->expects($this->exactly(2))
+ ->method('encrypt')
+ ->with($password)
+ ->willReturn('encrypted-' . $password);
+
+ $savedAccount = new MailAccount();
+ $savedAccount->setId(10);
+
+ $this->accountService->expects($this->once())
+ ->method('save')
+ ->willReturnCallback(function (MailAccount $account) use ($savedAccount, $emailAddress) {
+ // Verify IMAP settings
+ $this->assertEquals('imap.example.com', $account->getInboundHost());
+ $this->assertEquals(993, $account->getInboundPort());
+ $this->assertEquals('ssl', $account->getInboundSslMode());
+ $this->assertEquals($emailAddress, $account->getInboundUser());
+ $this->assertEquals('encrypted-secret-password', $account->getInboundPassword());
+
+ // Verify SMTP settings
+ $this->assertEquals('smtp.example.com', $account->getOutboundHost());
+ $this->assertEquals(465, $account->getOutboundPort());
+ $this->assertEquals('ssl', $account->getOutboundSslMode());
+ $this->assertEquals($emailAddress, $account->getOutboundUser());
+ $this->assertEquals('encrypted-secret-password', $account->getOutboundPassword());
+
+ return $savedAccount;
+ });
+
+ $this->logger->expects($this->exactly(3))
+ ->method('info');
+
+ $result = $this->service->createOrUpdateAccount($userId, $emailUser, $accountName);
+
+ $this->assertInstanceOf(MailAccount::class, $result);
+ }
+
+ private function createMailAccountConfig(string $emailAddress, string $password): MailAccountConfig {
+ $imapConfig = new MailServerConfig(
+ host: 'imap.example.com',
+ port: 993,
+ security: 'ssl',
+ username: $emailAddress,
+ password: $password,
+ );
+
+ $smtpConfig = new MailServerConfig(
+ host: 'smtp.example.com',
+ port: 465,
+ security: 'ssl',
+ username: $emailAddress,
+ password: $password,
+ );
+
+ return new MailAccountConfig(
+ email: $emailAddress,
+ imap: $imapConfig,
+ smtp: $smtpConfig,
+ );
+ }
+
+ /**
+ * Helper to create an account object with a MailAccount
+ * This simulates the structure returned by AccountService::findByUserIdAndAddress
+ */
+ private function createAccountWithMailAccount(MailAccount $mailAccount): object {
+ return new class($mailAccount) {
+ public function __construct(private MailAccount $mailAccount) {
+ }
+
+ public function getId(): int {
+ return $this->mailAccount->getId();
+ }
+
+ public function getEmail(): string {
+ return $this->mailAccount->getEmail();
+ }
+
+ public function getMailAccount(): MailAccount {
+ return $this->mailAccount;
+ }
+ };
+ }
+}
diff --git a/tests/Unit/Service/IONOS/IonosConfigServiceTest.php b/tests/Unit/Service/IONOS/IonosConfigServiceTest.php
index 9a3f6ea83e..8951981ecf 100644
--- a/tests/Unit/Service/IONOS/IonosConfigServiceTest.php
+++ b/tests/Unit/Service/IONOS/IonosConfigServiceTest.php
@@ -38,6 +38,10 @@ protected function setUp(): void {
);
}
+ public function testAppNameConstantExists(): void {
+ $this->assertSame('NEXTCLOUD_WORKSPACE', IonosConfigService::APP_NAME);
+ }
+
public function testGetExternalReferenceSuccess(): void {
$this->config->method('getSystemValue')
->with('ncw.ext_ref')
diff --git a/tests/Unit/Service/IONOS/IonosMailConfigServiceTest.php b/tests/Unit/Service/IONOS/IonosMailConfigServiceTest.php
index a05bdd9e82..e1fdcf980b 100644
--- a/tests/Unit/Service/IONOS/IonosMailConfigServiceTest.php
+++ b/tests/Unit/Service/IONOS/IonosMailConfigServiceTest.php
@@ -10,15 +10,20 @@
namespace OCA\Mail\Tests\Unit\Service\IONOS;
use ChristophWurst\Nextcloud\Testing\TestCase;
+use OCA\Mail\Service\AccountService;
use OCA\Mail\Service\IONOS\IonosConfigService;
use OCA\Mail\Service\IONOS\IonosMailConfigService;
use OCA\Mail\Service\IONOS\IonosMailService;
+use OCP\IUser;
+use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
class IonosMailConfigServiceTest extends TestCase {
private IonosConfigService&MockObject $ionosConfigService;
private IonosMailService&MockObject $ionosMailService;
+ private AccountService&MockObject $accountService;
+ private IUserSession&MockObject $userSession;
private LoggerInterface&MockObject $logger;
private IonosMailConfigService $service;
@@ -27,11 +32,15 @@ protected function setUp(): void {
$this->ionosConfigService = $this->createMock(IonosConfigService::class);
$this->ionosMailService = $this->createMock(IonosMailService::class);
+ $this->accountService = $this->createMock(AccountService::class);
+ $this->userSession = $this->createMock(IUserSession::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->service = new IonosMailConfigService(
$this->ionosConfigService,
$this->ionosMailService,
+ $this->accountService,
+ $this->userSession,
$this->logger,
);
}
@@ -41,58 +50,174 @@ public function testIsMailConfigAvailableReturnsFalseWhenFeatureDisabled(): void
->method('isIonosIntegrationEnabled')
->willReturn(false);
- $this->ionosMailService->expects($this->never())
- ->method('mailAccountExistsForCurrentUser');
+ $this->userSession->expects($this->never())
+ ->method('getUser');
$result = $this->service->isMailConfigAvailable();
$this->assertFalse($result);
}
- public function testIsMailConfigAvailableReturnsTrueWhenUserHasNoAccount(): void {
+ public function testIsMailConfigAvailableReturnsFalseWhenNoUserSession(): void {
$this->ionosConfigService->expects($this->once())
->method('isIonosIntegrationEnabled')
->willReturn(true);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn(null);
+
+ $this->logger->expects($this->once())
+ ->method('debug')
+ ->with('IONOS mail config not available - no user session');
+
+ $result = $this->service->isMailConfigAvailable();
+
+ $this->assertFalse($result);
+ }
+
+ public function testIsMailConfigAvailableReturnsTrueWhenUserHasNoRemoteAccount(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('testuser');
+
+ $this->ionosConfigService->expects($this->once())
+ ->method('isIonosIntegrationEnabled')
+ ->willReturn(true);
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
$this->ionosMailService->expects($this->once())
->method('mailAccountExistsForCurrentUser')
->willReturn(false);
- $this->logger->expects($this->never())
- ->method('debug');
+ $this->accountService->expects($this->never())
+ ->method('findByUserIdAndAddress');
$result = $this->service->isMailConfigAvailable();
$this->assertTrue($result);
}
- public function testIsMailConfigAvailableReturnsFalseWhenUserHasAccount(): void {
+ public function testIsMailConfigAvailableReturnsFalseWhenUserHasRemoteAndLocalAccount(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('testuser');
+
$this->ionosConfigService->expects($this->once())
->method('isIonosIntegrationEnabled')
->willReturn(true);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
$this->ionosMailService->expects($this->once())
->method('mailAccountExistsForCurrentUser')
->willReturn(true);
+ $this->ionosMailService->expects($this->once())
+ ->method('getIonosEmailForUser')
+ ->with('testuser')
+ ->willReturn('testuser@ionos.com');
+
+ // Return a non-empty array to simulate that a local account exists
+ $mockAccount = $this->createMock(\OCA\Mail\Account::class);
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with('testuser', 'testuser@ionos.com')
+ ->willReturn([$mockAccount]);
+
$this->logger->expects($this->once())
->method('debug')
- ->with('IONOS mail config not available - user already has an account');
+ ->with('IONOS mail config not available - user already has account configured locally', [
+ 'email' => 'testuser@ionos.com',
+ ]);
$result = $this->service->isMailConfigAvailable();
$this->assertFalse($result);
}
- public function testIsMailConfigAvailableReturnsFalseOnException(): void {
+ public function testIsMailConfigAvailableReturnsTrueWhenUserHasRemoteAccountButNotLocal(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('testuser');
+
$this->ionosConfigService->expects($this->once())
->method('isIonosIntegrationEnabled')
->willReturn(true);
- $exception = new \Exception('Test exception');
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
$this->ionosMailService->expects($this->once())
->method('mailAccountExistsForCurrentUser')
+ ->willReturn(true);
+
+ $this->ionosMailService->expects($this->once())
+ ->method('getIonosEmailForUser')
+ ->with('testuser')
+ ->willReturn('testuser@ionos.com');
+
+ $this->accountService->expects($this->once())
+ ->method('findByUserIdAndAddress')
+ ->with('testuser', 'testuser@ionos.com')
+ ->willReturn([]);
+
+ $this->logger->expects($this->once())
+ ->method('debug')
+ ->with('IONOS mail config available - remote account exists but not configured locally', [
+ 'email' => 'testuser@ionos.com',
+ ]);
+
+ $result = $this->service->isMailConfigAvailable();
+
+ $this->assertTrue($result);
+ }
+
+ public function testIsMailConfigAvailableReturnsFalseWhenEmailCannotBeRetrieved(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')->willReturn('testuser');
+
+ $this->ionosConfigService->expects($this->once())
+ ->method('isIonosIntegrationEnabled')
+ ->willReturn(true);
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+
+ $this->ionosMailService->expects($this->once())
+ ->method('mailAccountExistsForCurrentUser')
+ ->willReturn(true);
+
+ $this->ionosMailService->expects($this->once())
+ ->method('getIonosEmailForUser')
+ ->with('testuser')
+ ->willReturn(null);
+
+ $this->logger->expects($this->once())
+ ->method('warning')
+ ->with('IONOS remote account exists but email could not be retrieved');
+
+ $this->accountService->expects($this->never())
+ ->method('findByUserIdAndAddress');
+
+ $result = $this->service->isMailConfigAvailable();
+
+ $this->assertFalse($result);
+ }
+
+ public function testIsMailConfigAvailableReturnsFalseOnException(): void {
+ $this->ionosConfigService->expects($this->once())
+ ->method('isIonosIntegrationEnabled')
+ ->willReturn(true);
+
+ $exception = new \Exception('Test exception');
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
->willThrowException($exception);
$this->logger->expects($this->once())
diff --git a/tests/Unit/Service/IONOS/IonosMailServiceTest.php b/tests/Unit/Service/IONOS/IonosMailServiceTest.php
index 75ba54a88d..4d2db79546 100644
--- a/tests/Unit/Service/IONOS/IonosMailServiceTest.php
+++ b/tests/Unit/Service/IONOS/IonosMailServiceTest.php
@@ -13,6 +13,7 @@
use GuzzleHttp\ClientInterface;
use IONOS\MailConfigurationAPI\Client\Api\MailConfigurationAPIApi;
use IONOS\MailConfigurationAPI\Client\Model\Imap;
+use IONOS\MailConfigurationAPI\Client\Model\MailAccountCreatedResponse;
use IONOS\MailConfigurationAPI\Client\Model\MailAccountResponse;
use IONOS\MailConfigurationAPI\Client\Model\MailAddonErrorMessage;
use IONOS\MailConfigurationAPI\Client\Model\MailServer;
@@ -28,6 +29,20 @@
use Psr\Log\LoggerInterface;
class IonosMailServiceTest extends TestCase {
+ private const TEST_USER_ID = 'testuser123';
+ private const TEST_USER_NAME = 'test';
+ private const TEST_DOMAIN = 'example.com';
+ private const TEST_EMAIL = self::TEST_USER_NAME . '@' . self::TEST_DOMAIN;
+ private const TEST_PASSWORD = 'test-password';
+ private const TEST_EXT_REF = 'test-ext-ref';
+ private const TEST_API_BASE_URL = 'https://api.example.com';
+ private const TEST_BASIC_AUTH_USER = 'testuser';
+ private const TEST_BASIC_AUTH_PASSWORD = 'testpass';
+ private const IMAP_HOST = 'imap.example.com';
+ private const IMAP_PORT = 993;
+ private const SMTP_HOST = 'smtp.example.com';
+ private const SMTP_PORT = 587;
+
private ApiMailConfigClientService&MockObject $apiClientService;
private IonosConfigService&MockObject $configService;
private IUserSession&MockObject $userSession;
@@ -50,54 +65,102 @@ protected function setUp(): void {
);
}
- public function testCreateEmailAccountSuccess(): void {
- $userName = 'test';
- $domain = 'example.com';
- $emailAddress = $userName . '@' . $domain;
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
- $this->configService->method('getMailDomain')->willReturn($domain);
+ /**
+ * Setup standard config mocks with default values
+ */
+ private function setupConfigMocks(
+ string $externalReference = self::TEST_EXT_REF,
+ string $apiBaseUrl = self::TEST_API_BASE_URL,
+ bool $allowInsecure = false,
+ string $basicAuthUser = self::TEST_BASIC_AUTH_USER,
+ string $basicAuthPassword = self::TEST_BASIC_AUTH_PASSWORD,
+ string $mailDomain = self::TEST_DOMAIN,
+ ): void {
+ $this->configService->method('getExternalReference')->willReturn($externalReference);
+ $this->configService->method('getApiBaseUrl')->willReturn($apiBaseUrl);
+ $this->configService->method('getAllowInsecure')->willReturn($allowInsecure);
+ $this->configService->method('getBasicAuthUser')->willReturn($basicAuthUser);
+ $this->configService->method('getBasicAuthPassword')->willReturn($basicAuthPassword);
+ $this->configService->method('getMailDomain')->willReturn($mailDomain);
+ }
- // Mock user session
+ /**
+ * Setup user session with mock user
+ */
+ private function setupUserSession(string $userId): IUser&MockObject {
$user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
+ $user->method('getUID')->willReturn($userId);
$this->userSession->method('getUser')->willReturn($user);
+ return $user;
+ }
- // Mock API client
+ /**
+ * Setup API client mocks and return API instance
+ */
+ private function setupApiClient(bool $verifySSL = true): MailConfigurationAPIApi&MockObject {
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')
->with([
- 'auth' => ['testuser', 'testpass'],
- 'verify' => true,
+ 'auth' => [self::TEST_BASIC_AUTH_USER, self::TEST_BASIC_AUTH_PASSWORD],
+ 'verify' => $verifySSL,
])
->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')
- ->with($client, 'https://api.example.com')
+ ->with($client, self::TEST_API_BASE_URL)
->willReturn($apiInstance);
- // Mock API response - use getMockBuilder with onlyMethods for existing methods
+ return $apiInstance;
+ }
+
+ /**
+ * Create a mock IMAP server
+ */
+ private function createMockImapServer(
+ string $host = self::IMAP_HOST,
+ int $port = self::IMAP_PORT,
+ string $sslMode = 'ssl',
+ ): Imap&MockObject {
$imapServer = $this->getMockBuilder(Imap::class)
->disableOriginalConstructor()
->onlyMethods(['getHost', 'getPort', 'getSslMode'])
->getMock();
- $imapServer->method('getHost')->willReturn('imap.example.com');
- $imapServer->method('getPort')->willReturn(993);
- $imapServer->method('getSslMode')->willReturn('ssl');
+ $imapServer->method('getHost')->willReturn($host);
+ $imapServer->method('getPort')->willReturn($port);
+ $imapServer->method('getSslMode')->willReturn($sslMode);
+ return $imapServer;
+ }
+ /**
+ * Create a mock SMTP server
+ */
+ private function createMockSmtpServer(
+ string $host = self::SMTP_HOST,
+ int $port = self::SMTP_PORT,
+ string $sslMode = 'tls',
+ ): Smtp&MockObject {
$smtpServer = $this->getMockBuilder(Smtp::class)
->disableOriginalConstructor()
->onlyMethods(['getHost', 'getPort', 'getSslMode'])
->getMock();
- $smtpServer->method('getHost')->willReturn('smtp.example.com');
- $smtpServer->method('getPort')->willReturn(587);
- $smtpServer->method('getSslMode')->willReturn('tls');
+ $smtpServer->method('getHost')->willReturn($host);
+ $smtpServer->method('getPort')->willReturn($port);
+ $smtpServer->method('getSslMode')->willReturn($sslMode);
+ return $smtpServer;
+ }
+
+ /**
+ * Create a mock MailAccountResponse
+ */
+ private function createMockMailAccountResponse(
+ string $email = self::TEST_EMAIL,
+ string $password = self::TEST_PASSWORD,
+ ?string $imapSslMode = 'ssl',
+ ?string $smtpSslMode = 'tls',
+ ): MailAccountCreatedResponse&MockObject {
+ $imapServer = $this->createMockImapServer(self::IMAP_HOST, self::IMAP_PORT, $imapSslMode);
+ $smtpServer = $this->createMockSmtpServer(self::SMTP_HOST, self::SMTP_PORT, $smtpSslMode);
$mailServer = $this->getMockBuilder(MailServer::class)
->disableOriginalConstructor()
@@ -106,116 +169,89 @@ public function testCreateEmailAccountSuccess(): void {
$mailServer->method('getImap')->willReturn($imapServer);
$mailServer->method('getSmtp')->willReturn($smtpServer);
- $mailAccountResponse = $this->getMockBuilder(MailAccountResponse::class)
+ $mailAccountResponse = $this->getMockBuilder(MailAccountCreatedResponse::class)
->disableOriginalConstructor()
->onlyMethods(['getEmail', 'getPassword', 'getServer'])
->getMock();
- $mailAccountResponse->method('getEmail')->willReturn($emailAddress);
- $mailAccountResponse->method('getPassword')->willReturn('test-password');
+ $mailAccountResponse->method('getEmail')->willReturn($email);
+ $mailAccountResponse->method('getPassword')->willReturn($password);
$mailAccountResponse->method('getServer')->willReturn($mailServer);
- $apiInstance->method('createMailbox')->willReturn($mailAccountResponse);
+ return $mailAccountResponse;
+ }
- // Expect logging calls
- $this->logger->expects($this->exactly(4))
- ->method('debug');
+ public function testCreateEmailAccountSuccess(): void {
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
+ $apiInstance = $this->setupApiClient();
+
+ $mailAccountResponse = $this->createMockMailAccountResponse();
+ $apiInstance->method('createMailbox')->willReturn($mailAccountResponse);
+ $this->logger->expects($this->exactly(4))->method('debug');
$this->logger->expects($this->once())
->method('info')
- ->with('Successfully created IONOS mail account', $this->callback(function ($context) use ($emailAddress) {
- return $context['email'] === $emailAddress
- && $context['userId'] === 'testuser123'
- && $context['userName'] === 'test';
+ ->with('Successfully created IONOS mail account', $this->callback(function ($context) {
+ return $context['email'] === self::TEST_EMAIL
+ && $context['userId'] === self::TEST_USER_ID
+ && $context['userName'] === self::TEST_USER_NAME;
}));
- $result = $this->service->createEmailAccount($userName);
+ $result = $this->service->createEmailAccount(self::TEST_USER_NAME);
$this->assertInstanceOf(MailAccountConfig::class, $result);
- $this->assertEquals($emailAddress, $result->getEmail());
- $this->assertEquals('imap.example.com', $result->getImap()->getHost());
- $this->assertEquals(993, $result->getImap()->getPort());
+ $this->assertEquals(self::TEST_EMAIL, $result->getEmail());
+ $this->assertEquals(self::IMAP_HOST, $result->getImap()->getHost());
+ $this->assertEquals(self::IMAP_PORT, $result->getImap()->getPort());
$this->assertEquals('ssl', $result->getImap()->getSecurity());
- $this->assertEquals($emailAddress, $result->getImap()->getUsername());
- $this->assertEquals('test-password', $result->getImap()->getPassword());
- $this->assertEquals('smtp.example.com', $result->getSmtp()->getHost());
- $this->assertEquals(587, $result->getSmtp()->getPort());
+ $this->assertEquals(self::TEST_EMAIL, $result->getImap()->getUsername());
+ $this->assertEquals(self::TEST_PASSWORD, $result->getImap()->getPassword());
+ $this->assertEquals(self::SMTP_HOST, $result->getSmtp()->getHost());
+ $this->assertEquals(self::SMTP_PORT, $result->getSmtp()->getPort());
$this->assertEquals('tls', $result->getSmtp()->getSecurity());
- $this->assertEquals($emailAddress, $result->getSmtp()->getUsername());
- $this->assertEquals('test-password', $result->getSmtp()->getPassword());
+ $this->assertEquals(self::TEST_EMAIL, $result->getSmtp()->getUsername());
+ $this->assertEquals(self::TEST_PASSWORD, $result->getSmtp()->getPassword());
}
public function testCreateEmailAccountWithApiException(): void {
- $userName = 'test';
- $domain = 'example.com';
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
- $this->configService->method('getMailDomain')->willReturn($domain);
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
- // Mock user session
- $user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
- $this->userSession->method('getUser')->willReturn($user);
-
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw exception
$apiInstance->method('createMailbox')
->willThrowException(new \Exception('API call failed'));
- // Expect logging calls
- $this->logger->expects($this->exactly(2))
- ->method('debug');
-
+ $this->logger->expects($this->exactly(2))->method('debug');
$this->logger->expects($this->once())
->method('error')
- ->with('Exception when calling MailConfigurationAPIApi->createMailbox', $this->callback(function ($context) use ($userName) {
+ ->with('Exception when calling MailConfigurationAPIApi->createMailbox', $this->callback(function ($context) {
return isset($context['exception'])
- && $context['userId'] === 'testuser123'
- && $context['userName'] === $userName;
+ && $context['userId'] === self::TEST_USER_ID
+ && $context['userName'] === self::TEST_USER_NAME;
}));
$this->expectException(ServiceException::class);
$this->expectExceptionMessage('Failed to create ionos mail');
$this->expectExceptionCode(500);
- $this->service->createEmailAccount($userName);
+ $this->service->createEmailAccount(self::TEST_USER_NAME);
}
public function testCreateEmailAccountWithMailAddonErrorMessageResponse(): void {
- $userName = 'test';
- $domain = 'example.com';
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
- $this->configService->method('getMailDomain')->willReturn($domain);
-
- // Mock user session
- $user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
- $this->userSession->method('getUser')->willReturn($user);
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock MailAddonErrorMessage response
$errorMessage = $this->getMockBuilder(MailAddonErrorMessage::class)
->disableOriginalConstructor()
->onlyMethods(['getStatus', 'getMessage'])
@@ -225,86 +261,55 @@ public function testCreateEmailAccountWithMailAddonErrorMessageResponse(): void
$apiInstance->method('createMailbox')->willReturn($errorMessage);
- // Expect logging calls
- $this->logger->expects($this->exactly(2))
- ->method('debug');
-
+ $this->logger->expects($this->exactly(2))->method('debug');
$this->logger->expects($this->once())
->method('error')
- ->with('Failed to create ionos mail', $this->callback(function ($context) use ($userName) {
+ ->with('Failed to create ionos mail', $this->callback(function ($context) {
return $context['status code'] === MailAddonErrorMessage::STATUS__400_BAD_REQUEST
&& $context['message'] === 'Bad Request'
- && $context['userId'] === 'testuser123'
- && $context['userName'] === $userName;
+ && $context['userId'] === self::TEST_USER_ID
+ && $context['userName'] === self::TEST_USER_NAME;
}));
$this->expectException(ServiceException::class);
$this->expectExceptionMessage('Failed to create ionos mail');
$this->expectExceptionCode(400);
- $this->service->createEmailAccount($userName);
+ $this->service->createEmailAccount(self::TEST_USER_NAME);
}
public function testCreateEmailAccountWithUnknownResponseType(): void {
- $userName = 'test';
- $domain = 'example.com';
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
- $this->configService->method('getMailDomain')->willReturn($domain);
-
- // Mock user session
- $user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
- $this->userSession->method('getUser')->willReturn($user);
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock unknown response type (return a stdClass instead of expected types)
$unknownResponse = new \stdClass();
$apiInstance->method('createMailbox')->willReturn($unknownResponse);
- // Expect logging calls
- $this->logger->expects($this->exactly(2))
- ->method('debug');
-
+ $this->logger->expects($this->exactly(2))->method('debug');
$this->logger->expects($this->once())
->method('error')
- ->with('Failed to create ionos mail: Unknown response type', $this->callback(function ($context) use ($userName) {
- return $context['userId'] === 'testuser123'
- && $context['userName'] === $userName;
+ ->with('Failed to create ionos mail: Unknown response type', $this->callback(function ($context) {
+ return $context['userId'] === self::TEST_USER_ID
+ && $context['userName'] === self::TEST_USER_NAME;
}));
$this->expectException(ServiceException::class);
$this->expectExceptionMessage('Failed to create ionos mail');
$this->expectExceptionCode(500);
- $this->service->createEmailAccount($userName);
+ $this->service->createEmailAccount(self::TEST_USER_NAME);
}
public function testCreateEmailAccountWithNoUserSession(): void {
- $userName = 'test';
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
-
- // Mock no user session
+ $this->setupConfigMocks();
$this->userSession->method('getUser')->willReturn(null);
- // Expect logging call
$this->logger->expects($this->once())
->method('error')
->with('No user session found when attempting to create IONOS mail account');
@@ -312,74 +317,64 @@ public function testCreateEmailAccountWithNoUserSession(): void {
$this->expectException(ServiceException::class);
$this->expectExceptionMessage('No user session found');
- $this->service->createEmailAccount($userName);
+ $this->service->createEmailAccount(self::TEST_USER_NAME);
}
- /**
- * Test SSL mode normalization with various API response values
- *
- * @dataProvider sslModeNormalizationProvider
- */
- public function testSslModeNormalization(string $apiSslMode, string $expectedSecurity): void {
- $userName = 'test';
- $domain = 'example.com';
- $emailAddress = $userName . '@' . $domain;
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
- $this->configService->method('getMailDomain')->willReturn($domain);
-
- // Mock user session
- $user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
- $this->userSession->method('getUser')->willReturn($user);
+ public function testCreateEmailAccountForUserSuccess(): void {
+ $userId = 'admin123';
+ $this->setupConfigMocks();
+ $apiInstance = $this->setupApiClient();
- // Mock API client
- $client = $this->createMock(ClientInterface::class);
- $this->apiClientService->method('newClient')->willReturn($client);
+ // No user session needed for this method
- $apiInstance = $this->createMock(MailConfigurationAPIApi::class);
- $this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
+ $mailAccountResponse = $this->createMockMailAccountResponse();
+ $apiInstance->method('createMailbox')->willReturn($mailAccountResponse);
- // Mock API response with specific SSL mode
- $imapServer = $this->getMockBuilder(Imap::class)
- ->disableOriginalConstructor()
- ->onlyMethods(['getHost', 'getPort', 'getSslMode'])
- ->getMock();
- $imapServer->method('getHost')->willReturn('imap.example.com');
- $imapServer->method('getPort')->willReturn(993);
- $imapServer->method('getSslMode')->willReturn($apiSslMode);
+ $this->logger->expects($this->exactly(4))->method('debug');
+ $this->logger->expects($this->once())
+ ->method('info')
+ ->with('Successfully created IONOS mail account', $this->callback(function ($context) use ($userId) {
+ return $context['email'] === self::TEST_EMAIL
+ && $context['userId'] === $userId
+ && $context['userName'] === self::TEST_USER_NAME;
+ }));
- $smtpServer = $this->getMockBuilder(Smtp::class)
- ->disableOriginalConstructor()
- ->onlyMethods(['getHost', 'getPort', 'getSslMode'])
- ->getMock();
- $smtpServer->method('getHost')->willReturn('smtp.example.com');
- $smtpServer->method('getPort')->willReturn(587);
- $smtpServer->method('getSslMode')->willReturn($apiSslMode);
+ $result = $this->service->createEmailAccountForUser($userId, self::TEST_USER_NAME);
- $mailServer = $this->getMockBuilder(MailServer::class)
- ->disableOriginalConstructor()
- ->onlyMethods(['getImap', 'getSmtp'])
- ->getMock();
- $mailServer->method('getImap')->willReturn($imapServer);
- $mailServer->method('getSmtp')->willReturn($smtpServer);
+ $this->assertInstanceOf(MailAccountConfig::class, $result);
+ $this->assertEquals(self::TEST_EMAIL, $result->getEmail());
+ $this->assertEquals(self::IMAP_HOST, $result->getImap()->getHost());
+ $this->assertEquals(self::IMAP_PORT, $result->getImap()->getPort());
+ $this->assertEquals('ssl', $result->getImap()->getSecurity());
+ $this->assertEquals(self::TEST_EMAIL, $result->getImap()->getUsername());
+ $this->assertEquals(self::TEST_PASSWORD, $result->getImap()->getPassword());
+ $this->assertEquals(self::SMTP_HOST, $result->getSmtp()->getHost());
+ $this->assertEquals(self::SMTP_PORT, $result->getSmtp()->getPort());
+ $this->assertEquals('tls', $result->getSmtp()->getSecurity());
+ $this->assertEquals(self::TEST_EMAIL, $result->getSmtp()->getUsername());
+ $this->assertEquals(self::TEST_PASSWORD, $result->getSmtp()->getPassword());
+ }
- $mailAccountResponse = $this->getMockBuilder(MailAccountResponse::class)
- ->disableOriginalConstructor()
- ->onlyMethods(['getEmail', 'getPassword', 'getServer'])
- ->getMock();
- $mailAccountResponse->method('getEmail')->willReturn($emailAddress);
- $mailAccountResponse->method('getPassword')->willReturn('test-password');
- $mailAccountResponse->method('getServer')->willReturn($mailServer);
+ /**
+ * Test SSL mode normalization with various API response values
+ *
+ * @dataProvider sslModeNormalizationProvider
+ */
+ public function testSslModeNormalization(string $apiSslMode, string $expectedSecurity): void {
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
+ $apiInstance = $this->setupApiClient();
+
+ $mailAccountResponse = $this->createMockMailAccountResponse(
+ self::TEST_EMAIL,
+ self::TEST_PASSWORD,
+ $apiSslMode,
+ $apiSslMode
+ );
$apiInstance->method('createMailbox')->willReturn($mailAccountResponse);
- $result = $this->service->createEmailAccount($userName);
+ $result = $this->service->createEmailAccount(self::TEST_USER_NAME);
$this->assertEquals($expectedSecurity, $result->getImap()->getSecurity());
$this->assertEquals($expectedSecurity, $result->getSmtp()->getSecurity());
@@ -432,39 +427,26 @@ public static function sslModeNormalizationProvider(): array {
}
public function testMailAccountExistsForCurrentUserReturnsTrueWhenAccountExists(): void {
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
- // Mock user session
- $user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
- $this->userSession->method('getUser')->willReturn($user);
-
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API response with existing account
$mailAccountResponse = $this->getMockBuilder(MailAccountResponse::class)
->disableOriginalConstructor()
->onlyMethods(['getEmail'])
->getMock();
- $mailAccountResponse->method('getEmail')->willReturn('testuser@example.com');
+ $mailAccountResponse->method('getEmail')->willReturn(self::TEST_EMAIL);
$apiInstance->method('getFunctionalAccount')
- ->with('IONOS', 'test-ext-ref', 'testuser123')
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willReturn($mailAccountResponse);
- // Expect logging calls
- $this->logger->expects($this->exactly(2))
- ->method('debug');
+ $this->logger->expects($this->exactly(2))->method('debug');
$result = $this->service->mailAccountExistsForCurrentUser();
@@ -472,26 +454,15 @@ public function testMailAccountExistsForCurrentUserReturnsTrueWhenAccountExists(
}
public function testMailAccountExistsForCurrentUserReturnsFalseWhen404(): void {
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
-
- // Mock user session
- $user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
- $this->userSession->method('getUser')->willReturn($user);
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw 404 exception
$apiException = new \IONOS\MailConfigurationAPI\Client\ApiException(
'Not Found',
404,
@@ -500,12 +471,10 @@ public function testMailAccountExistsForCurrentUserReturnsFalseWhen404(): void {
);
$apiInstance->method('getFunctionalAccount')
- ->with('IONOS', 'test-ext-ref', 'testuser123')
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willThrowException($apiException);
- // Expect logging calls
- $this->logger->expects($this->exactly(2))
- ->method('debug');
+ $this->logger->expects($this->exactly(2))->method('debug');
$result = $this->service->mailAccountExistsForCurrentUser();
@@ -513,26 +482,15 @@ public function testMailAccountExistsForCurrentUserReturnsFalseWhen404(): void {
}
public function testMailAccountExistsForCurrentUserReturnsFalseOnApiError(): void {
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
-
- // Mock user session
- $user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
- $this->userSession->method('getUser')->willReturn($user);
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw 500 exception
$apiException = new \IONOS\MailConfigurationAPI\Client\ApiException(
'Internal Server Error',
500,
@@ -541,18 +499,15 @@ public function testMailAccountExistsForCurrentUserReturnsFalseOnApiError(): voi
);
$apiInstance->method('getFunctionalAccount')
- ->with('IONOS', 'test-ext-ref', 'testuser123')
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willThrowException($apiException);
- // Expect logging calls
- $this->logger->expects($this->once())
- ->method('debug');
-
+ $this->logger->expects($this->once())->method('debug');
$this->logger->expects($this->once())
->method('error')
->with('API Exception when getting IONOS mail account', $this->callback(function ($context) {
return $context['statusCode'] === 500
- && $context['message'] === 'Internal Server Error';
+ && $context['message'] === 'Internal Server Error';
}));
$result = $this->service->mailAccountExistsForCurrentUser();
@@ -561,39 +516,25 @@ public function testMailAccountExistsForCurrentUserReturnsFalseOnApiError(): voi
}
public function testMailAccountExistsForCurrentUserReturnsFalseOnGeneralException(): void {
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
-
- // Mock user session
- $user = $this->createMock(IUser::class);
- $user->method('getUID')->willReturn('testuser123');
- $this->userSession->method('getUser')->willReturn($user);
+ $this->setupConfigMocks();
+ $this->setupUserSession(self::TEST_USER_ID);
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw general exception
$apiInstance->method('getFunctionalAccount')
- ->with('IONOS', 'test-ext-ref', 'testuser123')
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willThrowException(new \Exception('Unexpected error'));
- // Expect logging calls
- $this->logger->expects($this->once())
- ->method('debug');
-
+ $this->logger->expects($this->once())->method('debug');
$this->logger->expects($this->once())
->method('error')
->with('Exception when getting IONOS mail account', $this->callback(function ($context) {
return isset($context['exception'])
- && $context['userId'] === 'testuser123';
+ && $context['userId'] === self::TEST_USER_ID;
}));
$result = $this->service->mailAccountExistsForCurrentUser();
@@ -603,73 +544,42 @@ public function testMailAccountExistsForCurrentUserReturnsFalseOnGeneralExceptio
public function testDeleteEmailAccountSuccess(): void {
- $userId = 'testuser123';
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
-
- // Mock API client
- $client = $this->createMock(ClientInterface::class);
- $this->apiClientService->method('newClient')
- ->with([
- 'auth' => ['testuser', 'testpass'],
- 'verify' => true,
- ])
- ->willReturn($client);
-
- $apiInstance = $this->createMock(MailConfigurationAPIApi::class);
- $this->apiClientService->method('newMailConfigurationAPIApi')
- ->with($client, 'https://api.example.com')
- ->willReturn($apiInstance);
+ $this->setupConfigMocks();
+ $apiInstance = $this->setupApiClient();
- // Mock successful deletion (returns void)
$apiInstance->expects($this->once())
->method('deleteMailbox')
- ->with('IONOS', 'test-ext-ref', $userId);
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID);
- // Expect logging calls
$callCount = 0;
$this->logger->expects($this->exactly(2))
->method('info')
- ->willReturnCallback(function ($message, $context) use ($userId, &$callCount) {
+ ->willReturnCallback(function ($message, $context) use (&$callCount) {
$callCount++;
if ($callCount === 1) {
$this->assertEquals('Attempting to delete IONOS email account', $message);
- $this->assertEquals($userId, $context['userId']);
- $this->assertEquals('test-ext-ref', $context['extRef']);
+ $this->assertEquals(self::TEST_USER_ID, $context['userId']);
+ $this->assertEquals(self::TEST_EXT_REF, $context['extRef']);
} elseif ($callCount === 2) {
$this->assertEquals('Successfully deleted IONOS email account', $message);
- $this->assertEquals($userId, $context['userId']);
+ $this->assertEquals(self::TEST_USER_ID, $context['userId']);
}
});
- $result = $this->service->deleteEmailAccount($userId);
+ $result = $this->service->deleteEmailAccount(self::TEST_USER_ID);
$this->assertTrue($result);
}
public function testDeleteEmailAccountReturns404AlreadyDeleted(): void {
- $userId = 'testuser123';
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
+ $this->setupConfigMocks();
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw 404 exception (mailbox doesn't exist)
$apiException = new \IONOS\MailConfigurationAPI\Client\ApiException(
'Not Found',
404,
@@ -679,48 +589,37 @@ public function testDeleteEmailAccountReturns404AlreadyDeleted(): void {
$apiInstance->expects($this->once())
->method('deleteMailbox')
- ->with('IONOS', 'test-ext-ref', $userId)
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willThrowException($apiException);
- // Expect logging calls
$this->logger->expects($this->once())
->method('info')
- ->with('Attempting to delete IONOS email account', $this->callback(function ($context) use ($userId) {
- return $context['userId'] === $userId
- && $context['extRef'] === 'test-ext-ref';
+ ->with('Attempting to delete IONOS email account', $this->callback(function ($context) {
+ return $context['userId'] === self::TEST_USER_ID
+ && $context['extRef'] === self::TEST_EXT_REF;
}));
$this->logger->expects($this->once())
->method('debug')
- ->with('IONOS mailbox does not exist (already deleted or never created)', $this->callback(function ($context) use ($userId) {
- return $context['userId'] === $userId
+ ->with('IONOS mailbox does not exist (already deleted or never created)', $this->callback(function ($context) {
+ return $context['userId'] === self::TEST_USER_ID
&& $context['statusCode'] === 404;
}));
- // Should return true for 404 (treat as success)
- $result = $this->service->deleteEmailAccount($userId);
+ $result = $this->service->deleteEmailAccount(self::TEST_USER_ID);
$this->assertTrue($result);
}
public function testDeleteEmailAccountThrowsExceptionOnApiError(): void {
- $userId = 'testuser123';
-
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
+ $this->setupConfigMocks();
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw 500 exception
$apiException = new \IONOS\MailConfigurationAPI\Client\ApiException(
'Internal Server Error',
500,
@@ -730,295 +629,206 @@ public function testDeleteEmailAccountThrowsExceptionOnApiError(): void {
$apiInstance->expects($this->once())
->method('deleteMailbox')
- ->with('IONOS', 'test-ext-ref', $userId)
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willThrowException($apiException);
- // Expect logging calls
$this->logger->expects($this->once())
->method('info')
- ->with('Attempting to delete IONOS email account', $this->callback(function ($context) use ($userId) {
- return $context['userId'] === $userId
- && $context['extRef'] === 'test-ext-ref';
+ ->with('Attempting to delete IONOS email account', $this->callback(function ($context) {
+ return $context['userId'] === self::TEST_USER_ID
+ && $context['extRef'] === self::TEST_EXT_REF;
}));
$this->logger->expects($this->once())
->method('error')
- ->with('API Exception when calling MailConfigurationAPIApi->deleteMailbox', $this->callback(function ($context) use ($userId) {
+ ->with('API Exception when calling MailConfigurationAPIApi->deleteMailbox', $this->callback(function ($context) {
return $context['statusCode'] === 500
&& $context['message'] === 'Internal Server Error'
- && $context['userId'] === $userId;
+ && $context['userId'] === self::TEST_USER_ID;
}));
$this->expectException(ServiceException::class);
$this->expectExceptionMessage('Failed to delete IONOS mail: Internal Server Error');
$this->expectExceptionCode(500);
- $this->service->deleteEmailAccount($userId);
+ $this->service->deleteEmailAccount(self::TEST_USER_ID);
}
public function testDeleteEmailAccountThrowsExceptionOnGeneralError(): void {
- $userId = 'testuser123';
+ $this->setupConfigMocks();
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
-
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw general exception
$generalException = new \Exception('Unexpected error');
$apiInstance->expects($this->once())
->method('deleteMailbox')
- ->with('IONOS', 'test-ext-ref', $userId)
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willThrowException($generalException);
- // Expect logging calls
$this->logger->expects($this->once())
->method('info')
- ->with('Attempting to delete IONOS email account', $this->callback(function ($context) use ($userId) {
- return $context['userId'] === $userId
- && $context['extRef'] === 'test-ext-ref';
+ ->with('Attempting to delete IONOS email account', $this->callback(function ($context) {
+ return $context['userId'] === self::TEST_USER_ID
+ && $context['extRef'] === self::TEST_EXT_REF;
}));
$this->logger->expects($this->once())
->method('error')
- ->with('Exception when calling MailConfigurationAPIApi->deleteMailbox', $this->callback(function ($context) use ($userId) {
+ ->with('Exception when calling MailConfigurationAPIApi->deleteMailbox', $this->callback(function ($context) {
return isset($context['exception'])
- && $context['userId'] === $userId;
+ && $context['userId'] === self::TEST_USER_ID;
}));
$this->expectException(ServiceException::class);
$this->expectExceptionMessage('Failed to delete IONOS mail');
$this->expectExceptionCode(500);
- $this->service->deleteEmailAccount($userId);
+ $this->service->deleteEmailAccount(self::TEST_USER_ID);
}
public function testDeleteEmailAccountWithInsecureConnection(): void {
- $userId = 'testuser123';
-
- // Mock config with insecure connection allowed
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(true);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
-
- // Mock API client - verify should be false
- $client = $this->createMock(ClientInterface::class);
- $this->apiClientService->method('newClient')
- ->with([
- 'auth' => ['testuser', 'testpass'],
- 'verify' => false,
- ])
- ->willReturn($client);
-
- $apiInstance = $this->createMock(MailConfigurationAPIApi::class);
- $this->apiClientService->method('newMailConfigurationAPIApi')
- ->with($client, 'https://api.example.com')
- ->willReturn($apiInstance);
+ $this->setupConfigMocks(allowInsecure: true);
+ $apiInstance = $this->setupApiClient(verifySSL: false);
- // Mock successful deletion
$apiInstance->expects($this->once())
->method('deleteMailbox')
- ->with('IONOS', 'test-ext-ref', $userId);
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID);
- $this->logger->expects($this->exactly(2))
- ->method('info');
+ $this->logger->expects($this->exactly(2))->method('info');
- $result = $this->service->deleteEmailAccount($userId);
+ $result = $this->service->deleteEmailAccount(self::TEST_USER_ID);
$this->assertTrue($result);
}
public function testTryDeleteEmailAccountWhenIntegrationDisabled(): void {
- $userId = 'testuser123';
-
- // Mock integration as disabled
$this->configService->expects($this->once())
->method('isIonosIntegrationEnabled')
->willReturn(false);
- // Should log that integration is not enabled
$this->logger->expects($this->once())
->method('debug')
->with(
'IONOS integration is not enabled, skipping email account deletion',
- ['userId' => $userId]
+ ['userId' => self::TEST_USER_ID]
);
- // Should not attempt to create API client
- $this->apiClientService->expects($this->never())
- ->method('newClient');
+ $this->apiClientService->expects($this->never())->method('newClient');
- // Call tryDeleteEmailAccount - should not throw exception
- $this->service->tryDeleteEmailAccount($userId);
+ $this->service->tryDeleteEmailAccount(self::TEST_USER_ID);
$this->addToAssertionCount(1);
}
public function testTryDeleteEmailAccountWhenIntegrationEnabledSuccess(): void {
- $userId = 'testuser123';
-
- // Mock integration as enabled
$this->configService->expects($this->once())
->method('isIonosIntegrationEnabled')
->willReturn(true);
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
+ $this->setupConfigMocks();
+ $apiInstance = $this->setupApiClient();
- // Mock API client
- $client = $this->createMock(ClientInterface::class);
- $this->apiClientService->method('newClient')
- ->with([
- 'auth' => ['testuser', 'testpass'],
- 'verify' => true,
- ])
- ->willReturn($client);
-
- $apiInstance = $this->createMock(MailConfigurationAPIApi::class);
- $this->apiClientService->method('newMailConfigurationAPIApi')
- ->with($client, 'https://api.example.com')
- ->willReturn($apiInstance);
-
- // Mock successful deletion
$apiInstance->expects($this->once())
->method('deleteMailbox')
- ->with('IONOS', 'test-ext-ref', $userId);
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID);
- // Should log success at info level (from deleteEmailAccount only)
$this->logger->expects($this->exactly(2))
->method('info')
- ->willReturnCallback(function ($message, $context) use ($userId) {
+ ->willReturnCallback(function ($message, $context) {
if ($message === 'Attempting to delete IONOS email account') {
- $this->assertSame($userId, $context['userId']);
- $this->assertSame('test-ext-ref', $context['extRef']);
+ $this->assertSame(self::TEST_USER_ID, $context['userId']);
+ $this->assertSame(self::TEST_EXT_REF, $context['extRef']);
} elseif ($message === 'Successfully deleted IONOS email account') {
- $this->assertSame($userId, $context['userId']);
+ $this->assertSame(self::TEST_USER_ID, $context['userId']);
}
});
- // Call tryDeleteEmailAccount - should not throw exception
- $this->service->tryDeleteEmailAccount($userId);
+ $this->service->tryDeleteEmailAccount(self::TEST_USER_ID);
$this->addToAssertionCount(1);
}
public function testTryDeleteEmailAccountWhenIntegrationEnabledButDeletionFails(): void {
- $userId = 'testuser123';
-
- // Mock integration as enabled
$this->configService->expects($this->once())
->method('isIonosIntegrationEnabled')
->willReturn(true);
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
+ $this->setupConfigMocks();
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')
->with([
- 'auth' => ['testuser', 'testpass'],
+ 'auth' => [self::TEST_BASIC_AUTH_USER, self::TEST_BASIC_AUTH_PASSWORD],
'verify' => true,
])
->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')
- ->with($client, 'https://api.example.com')
+ ->with($client, self::TEST_API_BASE_URL)
->willReturn($apiInstance);
- // Mock API exception
$apiException = new \IONOS\MailConfigurationAPI\Client\ApiException('API Error', 500);
$apiInstance->expects($this->once())
->method('deleteMailbox')
- ->with('IONOS', 'test-ext-ref', $userId)
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willThrowException($apiException);
- // Should log the error from deleteEmailAccount and then from tryDeleteEmailAccount
$this->logger->expects($this->exactly(2))
->method('error')
- ->willReturnCallback(function ($message, $context) use ($userId) {
+ ->willReturnCallback(function ($message, $context) {
if ($message === 'API Exception when calling MailConfigurationAPIApi->deleteMailbox') {
- // This is from deleteEmailAccount
- $this->assertSame($userId, $context['userId']);
+ $this->assertSame(self::TEST_USER_ID, $context['userId']);
$this->assertSame(500, $context['statusCode']);
} elseif ($message === 'Failed to delete IONOS mailbox for user') {
- // This is from tryDeleteEmailAccount
- $this->assertSame($userId, $context['userId']);
+ $this->assertSame(self::TEST_USER_ID, $context['userId']);
$this->assertInstanceOf(ServiceException::class, $context['exception']);
}
});
- // Call tryDeleteEmailAccount - should NOT throw exception (fire and forget)
- $this->service->tryDeleteEmailAccount($userId);
+ $this->service->tryDeleteEmailAccount(self::TEST_USER_ID);
$this->addToAssertionCount(1);
}
public function testTryDeleteEmailAccountWhenMailboxNotFound(): void {
- $userId = 'testuser123';
-
- // Mock integration as enabled
$this->configService->expects($this->once())
->method('isIonosIntegrationEnabled')
->willReturn(true);
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
+ $this->setupConfigMocks();
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')
->with([
- 'auth' => ['testuser', 'testpass'],
+ 'auth' => [self::TEST_BASIC_AUTH_USER, self::TEST_BASIC_AUTH_PASSWORD],
'verify' => true,
])
->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')
- ->with($client, 'https://api.example.com')
+ ->with($client, self::TEST_API_BASE_URL)
->willReturn($apiInstance);
- // Mock 404 API exception (mailbox already deleted or never existed)
$apiException = new \IONOS\MailConfigurationAPI\Client\ApiException('Not Found', 404);
$apiInstance->expects($this->once())
->method('deleteMailbox')
- ->with('IONOS', 'test-ext-ref', $userId)
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
->willThrowException($apiException);
- // Should log at info level (from deleteEmailAccount) and debug (404 is treated as success)
$this->logger->expects($this->once())
->method('info')
->with(
'Attempting to delete IONOS email account',
[
- 'userId' => $userId,
- 'extRef' => 'test-ext-ref',
+ 'userId' => self::TEST_USER_ID,
+ 'extRef' => self::TEST_EXT_REF,
]
);
@@ -1027,20 +837,131 @@ public function testTryDeleteEmailAccountWhenMailboxNotFound(): void {
->with(
'IONOS mailbox does not exist (already deleted or never created)',
[
- 'userId' => $userId,
+ 'userId' => self::TEST_USER_ID,
'statusCode' => 404
]
);
- // Call tryDeleteEmailAccount - should NOT throw exception
- $this->service->tryDeleteEmailAccount($userId);
+ $this->service->tryDeleteEmailAccount(self::TEST_USER_ID);
$this->addToAssertionCount(1);
}
public function testGetIonosEmailForUserReturnsEmailWhenAccountExists(): void {
+ $this->setupConfigMocks();
+
+ $client = $this->createMock(ClientInterface::class);
+ $this->apiClientService->method('newClient')->willReturn($client);
+
+ $apiInstance = $this->createMock(MailConfigurationAPIApi::class);
+ $this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
+
+ $mailAccountResponse = $this->getMockBuilder(MailAccountResponse::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getEmail'])
+ ->getMock();
+ $mailAccountResponse->method('getEmail')->willReturn(self::TEST_EMAIL);
+
+ $apiInstance->method('getFunctionalAccount')
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
+ ->willReturn($mailAccountResponse);
+
+ $this->logger->expects($this->exactly(2))->method('debug');
+
+ $result = $this->service->getIonosEmailForUser(self::TEST_USER_ID);
+
+ $this->assertEquals(self::TEST_EMAIL, $result);
+ }
+
+ public function testGetIonosEmailForUserReturnsNullWhen404(): void {
+ $this->setupConfigMocks();
+
+ $client = $this->createMock(ClientInterface::class);
+ $this->apiClientService->method('newClient')->willReturn($client);
+
+ $apiInstance = $this->createMock(MailConfigurationAPIApi::class);
+ $this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
+
+ $apiException = new \IONOS\MailConfigurationAPI\Client\ApiException(
+ 'Not Found',
+ 404,
+ [],
+ '{"error": "Not Found"}'
+ );
+
+ $apiInstance->method('getFunctionalAccount')
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
+ ->willThrowException($apiException);
+
+ $this->logger->expects($this->exactly(2))->method('debug');
+
+ $result = $this->service->getIonosEmailForUser(self::TEST_USER_ID);
+
+ $this->assertNull($result);
+ }
+
+ public function testGetIonosEmailForUserReturnsNullOnApiError(): void {
+ $this->setupConfigMocks();
+
+ $client = $this->createMock(ClientInterface::class);
+ $this->apiClientService->method('newClient')->willReturn($client);
+
+ $apiInstance = $this->createMock(MailConfigurationAPIApi::class);
+ $this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
+
+ $apiException = new \IONOS\MailConfigurationAPI\Client\ApiException(
+ 'Internal Server Error',
+ 500,
+ [],
+ '{"error": "Server error"}'
+ );
+
+ $apiInstance->method('getFunctionalAccount')
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
+ ->willThrowException($apiException);
+
+ $this->logger->expects($this->once())->method('debug');
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with('API Exception when getting IONOS mail account', $this->callback(function ($context) {
+ return $context['statusCode'] === 500
+ && $context['message'] === 'Internal Server Error';
+ }));
+
+ $result = $this->service->getIonosEmailForUser(self::TEST_USER_ID);
+
+ $this->assertNull($result);
+ }
+
+ public function testGetIonosEmailForUserReturnsNullOnGeneralException(): void {
+ $this->setupConfigMocks();
+
+ $client = $this->createMock(ClientInterface::class);
+ $this->apiClientService->method('newClient')->willReturn($client);
+
+ $apiInstance = $this->createMock(MailConfigurationAPIApi::class);
+ $this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
+
+ $apiInstance->method('getFunctionalAccount')
+ ->with('IONOS', self::TEST_EXT_REF, self::TEST_USER_ID)
+ ->willThrowException(new \Exception('Unexpected error'));
+
+ $this->logger->expects($this->once())->method('debug');
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with('Exception when getting IONOS mail account', $this->callback(function ($context) {
+ return isset($context['exception'])
+ && $context['userId'] === self::TEST_USER_ID;
+ }));
+
+ $result = $this->service->getIonosEmailForUser(self::TEST_USER_ID);
+
+ $this->assertNull($result);
+ }
+
+ public function testGetAccountConfigForUserReturnsConfigWhenAccountExists(): void {
$userId = 'testuser123';
- $expectedEmail = 'testuser@example.com';
+ $emailAddress = 'testuser@example.com';
// Mock config
$this->configService->method('getExternalReference')->willReturn('test-ext-ref');
@@ -1057,26 +978,72 @@ public function testGetIonosEmailForUserReturnsEmailWhenAccountExists(): void {
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
// Mock API response with existing account
+ $imapServer = $this->getMockBuilder(Imap::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getHost', 'getPort', 'getSslMode'])
+ ->getMock();
+ $imapServer->method('getHost')->willReturn('imap.example.com');
+ $imapServer->method('getPort')->willReturn(993);
+ $imapServer->method('getSslMode')->willReturn('ssl');
+
+ $smtpServer = $this->getMockBuilder(Smtp::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getHost', 'getPort', 'getSslMode'])
+ ->getMock();
+ $smtpServer->method('getHost')->willReturn('smtp.example.com');
+ $smtpServer->method('getPort')->willReturn(587);
+ $smtpServer->method('getSslMode')->willReturn('tls');
+
+ $mailServer = $this->getMockBuilder(MailServer::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['getImap', 'getSmtp'])
+ ->getMock();
+ $mailServer->method('getImap')->willReturn($imapServer);
+ $mailServer->method('getSmtp')->willReturn($smtpServer);
+
$mailAccountResponse = $this->getMockBuilder(MailAccountResponse::class)
->disableOriginalConstructor()
- ->onlyMethods(['getEmail'])
+ ->onlyMethods(['getEmail', 'getServer'])
->getMock();
- $mailAccountResponse->method('getEmail')->willReturn($expectedEmail);
+ $mailAccountResponse->method('getEmail')->willReturn($emailAddress);
+ $mailAccountResponse->method('getServer')->willReturn($mailServer);
$apiInstance->method('getFunctionalAccount')
->with('IONOS', 'test-ext-ref', $userId)
->willReturn($mailAccountResponse);
// Expect logging calls
- $this->logger->expects($this->exactly(2))
+ // 1 debug from getMailAccountResponse (getting account)
+ // 2 debug from buildConfigFromAccountResponse (normalizeSslMode for IMAP and SMTP)
+ // 1 info from getAccountConfigForUser
+ $this->logger->expects($this->exactly(3))
->method('debug');
- $result = $this->service->getIonosEmailForUser($userId);
+ $this->logger->expects($this->once())
+ ->method('info')
+ ->with('Retrieved existing IONOS account configuration', $this->callback(function ($context) use ($emailAddress, $userId) {
+ return $context['email'] === $emailAddress
+ && $context['userId'] === $userId;
+ }));
- $this->assertEquals($expectedEmail, $result);
+ $result = $this->service->getAccountConfigForUser($userId);
+
+ $this->assertInstanceOf(MailAccountConfig::class, $result);
+ $this->assertEquals($emailAddress, $result->getEmail());
+ $this->assertEquals('imap.example.com', $result->getImap()->getHost());
+ $this->assertEquals(993, $result->getImap()->getPort());
+ $this->assertEquals('ssl', $result->getImap()->getSecurity());
+ $this->assertEquals($emailAddress, $result->getImap()->getUsername());
+ // Password is not available when retrieving existing accounts (security)
+ $this->assertEquals('', $result->getImap()->getPassword());
+ $this->assertEquals('smtp.example.com', $result->getSmtp()->getHost());
+ $this->assertEquals(587, $result->getSmtp()->getPort());
+ $this->assertEquals('tls', $result->getSmtp()->getSecurity());
+ $this->assertEquals($emailAddress, $result->getSmtp()->getUsername());
+ $this->assertEquals('', $result->getSmtp()->getPassword());
}
- public function testGetIonosEmailForUserReturnsNullWhen404(): void {
+ public function testGetAccountConfigForUserReturnsNullWhenAccountDoesNotExist(): void {
$userId = 'testuser123';
// Mock config
@@ -1093,7 +1060,7 @@ public function testGetIonosEmailForUserReturnsNullWhen404(): void {
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw 404 exception
+ // Mock API to throw 404 exception (account doesn't exist)
$apiException = new \IONOS\MailConfigurationAPI\Client\ApiException(
'Not Found',
404,
@@ -1106,15 +1073,29 @@ public function testGetIonosEmailForUserReturnsNullWhen404(): void {
->willThrowException($apiException);
// Expect logging calls
- $this->logger->expects($this->exactly(2))
- ->method('debug');
+ // 1 debug from getMailAccountResponse (getting account)
+ // 1 debug from getMailAccountResponse catch block (404 not found)
+ // 1 debug from getAccountConfigForUser (no existing account)
+ $this->logger->expects($this->exactly(3))
+ ->method('debug')
+ ->willReturnCallback(function ($message, $context) use ($userId) {
+ if ($message === 'Getting IONOS mail account for user') {
+ $this->assertEquals($userId, $context['userId']);
+ $this->assertEquals('test-ext-ref', $context['extRef']);
+ } elseif ($message === 'User does not have IONOS mail account') {
+ $this->assertEquals($userId, $context['userId']);
+ $this->assertEquals(404, $context['statusCode']);
+ } elseif ($message === 'No existing IONOS account found for user') {
+ $this->assertEquals($userId, $context['userId']);
+ }
+ });
- $result = $this->service->getIonosEmailForUser($userId);
+ $result = $this->service->getAccountConfigForUser($userId);
$this->assertNull($result);
}
- public function testGetIonosEmailForUserReturnsNullOnApiError(): void {
+ public function testGetAccountConfigForUserReturnsNullOnApiError(): void {
$userId = 'testuser123';
// Mock config
@@ -1144,56 +1125,135 @@ public function testGetIonosEmailForUserReturnsNullOnApiError(): void {
->willThrowException($apiException);
// Expect logging calls
- $this->logger->expects($this->once())
- ->method('debug');
+ // 1 debug from getMailAccountResponse (getting account)
+ // 1 debug from getAccountConfigForUser (no existing account)
+ $this->logger->expects($this->exactly(2))
+ ->method('debug')
+ ->willReturnCallback(function ($message, $context) use ($userId) {
+ if ($message === 'Getting IONOS mail account for user') {
+ $this->assertEquals($userId, $context['userId']);
+ $this->assertEquals('test-ext-ref', $context['extRef']);
+ } elseif ($message === 'No existing IONOS account found for user') {
+ $this->assertEquals($userId, $context['userId']);
+ }
+ });
$this->logger->expects($this->once())
->method('error')
- ->with('API Exception when getting IONOS mail account', $this->callback(function ($context) {
+ ->with('API Exception when getting IONOS mail account', $this->callback(function ($context) use ($userId) {
return $context['statusCode'] === 500
- && $context['message'] === 'Internal Server Error';
+ && $context['message'] === 'Internal Server Error'
+ && $context['userId'] === $userId;
}));
- $result = $this->service->getIonosEmailForUser($userId);
+ $result = $this->service->getAccountConfigForUser($userId);
$this->assertNull($result);
}
- public function testGetIonosEmailForUserReturnsNullOnGeneralException(): void {
+ public function testResetAppPasswordSuccess(): void {
$userId = 'testuser123';
+ $appName = 'NEXTCLOUD_WORKSPACE';
+ $expectedPassword = 'new-app-password-123';
- // Mock config
- $this->configService->method('getExternalReference')->willReturn('test-ext-ref');
- $this->configService->method('getApiBaseUrl')->willReturn('https://api.example.com');
- $this->configService->method('getAllowInsecure')->willReturn(false);
- $this->configService->method('getBasicAuthUser')->willReturn('testuser');
- $this->configService->method('getBasicAuthPassword')->willReturn('testpass');
+ $this->setupConfigMocks();
+ $apiInstance = $this->setupApiClient();
+
+ $apiInstance->expects($this->once())
+ ->method('setAppPassword')
+ ->with('IONOS', self::TEST_EXT_REF, $userId, $appName)
+ ->willReturn($expectedPassword);
+
+ $this->logger->expects($this->once())
+ ->method('debug')
+ ->with('Resetting IONOS app password', $this->callback(function ($context) use ($userId, $appName) {
+ return $context['userId'] === $userId
+ && $context['appName'] === $appName
+ && $context['extRef'] === self::TEST_EXT_REF;
+ }));
+
+ $this->logger->expects($this->once())
+ ->method('info')
+ ->with('Successfully reset IONOS app password', $this->callback(function ($context) use ($userId, $appName) {
+ return $context['userId'] === $userId
+ && $context['appName'] === $appName;
+ }));
+
+ $result = $this->service->resetAppPassword($userId, $appName);
+
+ $this->assertEquals($expectedPassword, $result);
+ }
+
+ public function testResetAppPasswordWithApiException(): void {
+ $userId = 'testuser123';
+ $appName = 'NEXTCLOUD_WORKSPACE';
+
+ $this->setupConfigMocks();
- // Mock API client
$client = $this->createMock(ClientInterface::class);
$this->apiClientService->method('newClient')->willReturn($client);
$apiInstance = $this->createMock(MailConfigurationAPIApi::class);
$this->apiClientService->method('newMailConfigurationAPIApi')->willReturn($apiInstance);
- // Mock API to throw general exception
- $apiInstance->method('getFunctionalAccount')
- ->with('IONOS', 'test-ext-ref', $userId)
- ->willThrowException(new \Exception('Unexpected error'));
+ $apiException = new \IONOS\MailConfigurationAPI\Client\ApiException(
+ 'Not Found',
+ 404,
+ [],
+ '{"error": "Mailbox not found"}'
+ );
+
+ $apiInstance->method('setAppPassword')
+ ->with('IONOS', self::TEST_EXT_REF, $userId, $appName)
+ ->willThrowException($apiException);
- // Expect logging calls
$this->logger->expects($this->once())
- ->method('debug');
+ ->method('debug')
+ ->with('Resetting IONOS app password', $this->anything());
$this->logger->expects($this->once())
->method('error')
- ->with('Exception when getting IONOS mail account', $this->callback(function ($context) {
- return isset($context['exception'])
- && $context['userId'] === 'testuser123';
+ ->with('API Exception when calling MailConfigurationAPIApi->setAppPassword', $this->callback(function ($context) use ($userId, $appName) {
+ return $context['statusCode'] === 404
+ && $context['message'] === 'Not Found'
+ && $context['userId'] === $userId
+ && $context['appName'] === $appName;
}));
- $result = $this->service->getIonosEmailForUser($userId);
+ $this->expectException(ServiceException::class);
+ $this->expectExceptionMessage('Failed to reset IONOS app password: Not Found');
+ $this->expectExceptionCode(404);
- $this->assertNull($result);
+ $this->service->resetAppPassword($userId, $appName);
+ }
+
+ public function testResetAppPasswordWithUnexpectedResponse(): void {
+ $userId = 'testuser123';
+ $appName = 'NEXTCLOUD_WORKSPACE';
+
+ $this->setupConfigMocks();
+ $apiInstance = $this->setupApiClient();
+
+ // API returns unexpected response type (not a string)
+ $apiInstance->method('setAppPassword')
+ ->with('IONOS', self::TEST_EXT_REF, $userId, $appName)
+ ->willReturn(['unexpected' => 'response']);
+
+ $this->logger->expects($this->once())
+ ->method('debug')
+ ->with('Resetting IONOS app password', $this->anything());
+
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with('Failed to reset IONOS app password: Unexpected response type', $this->callback(function ($context) use ($userId, $appName) {
+ return $context['userId'] === $userId
+ && $context['appName'] === $appName;
+ }));
+
+ $this->expectException(ServiceException::class);
+ $this->expectExceptionMessage('Failed to reset IONOS app password');
+ $this->expectExceptionCode(500);
+
+ $this->service->resetAppPassword($userId, $appName);
}
}
diff --git a/tests/Unit/Service/SetupServiceTest.php b/tests/Unit/Service/SetupServiceTest.php
new file mode 100644
index 0000000000..8637b2c1ff
--- /dev/null
+++ b/tests/Unit/Service/SetupServiceTest.php
@@ -0,0 +1,386 @@
+accountService = $this->createMock(AccountService::class);
+ $this->crypto = $this->createMock(ICrypto::class);
+ $this->smtpClientFactory = $this->createMock(SmtpClientFactory::class);
+ $this->imapClientFactory = $this->createMock(IMAPClientFactory::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->tagMapper = $this->createMock(TagMapper::class);
+
+ $this->setupService = new SetupService(
+ $this->accountService,
+ $this->crypto,
+ $this->smtpClientFactory,
+ $this->imapClientFactory,
+ $this->logger,
+ $this->tagMapper
+ );
+ }
+
+ private function mockSuccessfulImapConnection(): Horde_Imap_Client_Socket&MockObject {
+ $imapClient = $this->createMock(Horde_Imap_Client_Socket::class);
+ $imapClient->expects(self::once())->method('login');
+ $imapClient->expects(self::once())->method('logout');
+
+ $this->imapClientFactory->expects(self::once())
+ ->method('getClient')
+ ->willReturn($imapClient);
+
+ return $imapClient;
+ }
+
+ private function mockSuccessfulSmtpConnection(): Horde_Mail_Transport_Smtphorde&MockObject {
+ $smtpTransport = $this->createMock(Horde_Mail_Transport_Smtphorde::class);
+ $smtpTransport->expects(self::once())->method('getSMTPObject');
+
+ $this->smtpClientFactory->expects(self::once())
+ ->method('create')
+ ->willReturn($smtpTransport);
+
+ return $smtpTransport;
+ }
+
+ private function mockPasswordEncryption(): void {
+ $this->crypto->expects(self::exactly(2))
+ ->method('encrypt')
+ ->willReturnOnConsecutiveCalls('encrypted-imap-password', 'encrypted-smtp-password');
+ }
+
+ private function assertAccountPropertiesMatch(
+ MailAccount $account,
+ string $accountName,
+ string $emailAddress,
+ string $imapHost,
+ int $imapPort,
+ string $imapSslMode,
+ string $imapUser,
+ string $smtpHost,
+ int $smtpPort,
+ string $smtpSslMode,
+ string $smtpUser,
+ string $uid,
+ string $authMethod,
+ ): void {
+ self::assertSame($accountName, $account->getName(), 'Account name does not match');
+ self::assertSame($emailAddress, $account->getEmail(), 'Email address does not match');
+ self::assertSame($imapHost, $account->getInboundHost(), 'IMAP host does not match');
+ self::assertSame($imapPort, $account->getInboundPort(), 'IMAP port does not match');
+ self::assertSame($imapSslMode, $account->getInboundSslMode(), 'IMAP SSL mode does not match');
+ self::assertSame($imapUser, $account->getInboundUser(), 'IMAP user does not match');
+ self::assertSame($smtpHost, $account->getOutboundHost(), 'SMTP host does not match');
+ self::assertSame($smtpPort, $account->getOutboundPort(), 'SMTP port does not match');
+ self::assertSame($smtpSslMode, $account->getOutboundSslMode(), 'SMTP SSL mode does not match');
+ self::assertSame($smtpUser, $account->getOutboundUser(), 'SMTP user does not match');
+ self::assertSame($uid, $account->getUserId(), 'User ID does not match');
+ self::assertSame($authMethod, $account->getAuthMethod(), 'Auth method does not match');
+ }
+
+ public function testCreateNewAccountWithPasswordAuth(): void {
+ $this->mockPasswordEncryption();
+
+ $this->logger->expects(self::once())
+ ->method('info')
+ ->with('Setting up manually configured account');
+
+ $debugCalls = [];
+ $this->logger->expects(self::exactly(2))
+ ->method('debug')
+ ->willReturnCallback(function (string $message, array $context = []) use (&$debugCalls): void {
+ $debugCalls[] = ['message' => $message, 'context' => $context];
+ });
+
+ $this->mockSuccessfulImapConnection();
+ $this->mockSuccessfulSmtpConnection();
+
+ $this->accountService->expects(self::once())
+ ->method('save')
+ ->with(self::callback(function (MailAccount $account): bool {
+ $this->assertAccountPropertiesMatch(
+ $account,
+ self::ACCOUNT_NAME,
+ self::EMAIL_ADDRESS,
+ self::IMAP_HOST,
+ self::IMAP_PORT,
+ self::IMAP_SSL_MODE,
+ self::IMAP_USER,
+ self::SMTP_HOST,
+ self::SMTP_PORT,
+ self::SMTP_SSL_MODE,
+ self::SMTP_USER,
+ self::USER_ID,
+ self::AUTH_METHOD_PASSWORD
+ );
+ return true;
+ }));
+
+ $this->tagMapper->expects(self::once())
+ ->method('createDefaultTags')
+ ->with(self::isInstanceOf(MailAccount::class));
+
+ $result = $this->setupService->createNewAccount(
+ self::ACCOUNT_NAME,
+ self::EMAIL_ADDRESS,
+ self::IMAP_HOST,
+ self::IMAP_PORT,
+ self::IMAP_SSL_MODE,
+ self::IMAP_USER,
+ self::IMAP_PASSWORD,
+ self::SMTP_HOST,
+ self::SMTP_PORT,
+ self::SMTP_SSL_MODE,
+ self::SMTP_USER,
+ self::SMTP_PASSWORD,
+ self::USER_ID,
+ self::AUTH_METHOD_PASSWORD
+ );
+
+ self::assertInstanceOf(Account::class, $result);
+
+ // Verify debug log calls
+ self::assertCount(2, $debugCalls);
+ self::assertSame('Connecting to account {account}', $debugCalls[0]['message']);
+ self::assertSame(['account' => self::EMAIL_ADDRESS], $debugCalls[0]['context']);
+ self::assertStringContainsString('account created ', $debugCalls[1]['message']);
+ self::assertSame([], $debugCalls[1]['context']);
+ }
+
+ public function testCreateNewAccountWithOAuth2(): void {
+ $this->crypto->expects(self::never())->method('encrypt');
+
+ $this->logger->expects(self::once())
+ ->method('info')
+ ->with('Setting up manually configured account');
+ $this->logger->expects(self::once())
+ ->method('debug')
+ ->with(self::stringContains('account created '));
+
+ $this->imapClientFactory->expects(self::never())->method('getClient');
+ $this->smtpClientFactory->expects(self::never())->method('create');
+
+ $this->accountService->expects(self::once())
+ ->method('save')
+ ->with(self::callback(function (MailAccount $account): bool {
+ return $account->getAuthMethod() === self::AUTH_METHOD_OAUTH2;
+ }));
+
+ $this->tagMapper->expects(self::once())->method('createDefaultTags');
+
+ $result = $this->setupService->createNewAccount(
+ 'OAuth2 Account',
+ 'oauth@example.com',
+ self::IMAP_HOST,
+ self::IMAP_PORT,
+ self::IMAP_SSL_MODE,
+ 'oauth@example.com',
+ null,
+ self::SMTP_HOST,
+ self::SMTP_PORT,
+ self::SMTP_SSL_MODE,
+ 'oauth@example.com',
+ null,
+ 'user456',
+ self::AUTH_METHOD_OAUTH2
+ );
+
+ self::assertInstanceOf(Account::class, $result);
+ }
+
+ public function testCreateNewAccountWithSkipConnectivityTest(): void {
+ $accountName = 'Skip Test Account';
+ $emailAddress = 'skip@example.com';
+ $imapHost = 'imap.example.com';
+ $imapPort = 993;
+ $imapSslMode = 'ssl';
+ $imapUser = 'skip@example.com';
+ $imapPassword = 'password';
+ $smtpHost = 'smtp.example.com';
+ $smtpPort = 465;
+ $smtpSslMode = 'ssl';
+ $smtpUser = 'skip@example.com';
+ $smtpPassword = 'password';
+ $uid = 'user789';
+ $authMethod = 'password';
+ $skipConnectivityTest = true;
+
+ $this->crypto->expects(self::exactly(2))
+ ->method('encrypt')
+ ->willReturnOnConsecutiveCalls('encrypted1', 'encrypted2');
+
+ $this->imapClientFactory->expects(self::never())
+ ->method('getClient');
+
+ $this->smtpClientFactory->expects(self::never())
+ ->method('create');
+
+ $this->accountService->expects(self::once())
+ ->method('save');
+
+ $this->tagMapper->expects(self::once())
+ ->method('createDefaultTags');
+
+ $result = $this->setupService->createNewAccount(
+ $accountName,
+ $emailAddress,
+ $imapHost,
+ $imapPort,
+ $imapSslMode,
+ $imapUser,
+ $imapPassword,
+ $smtpHost,
+ $smtpPort,
+ $smtpSslMode,
+ $smtpUser,
+ $smtpPassword,
+ $uid,
+ $authMethod,
+ null,
+ $skipConnectivityTest
+ );
+
+ self::assertInstanceOf(Account::class, $result);
+ }
+
+ public function testCreateNewAccountWithInvalidAuthMethod(): void {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Invalid auth method invalid');
+
+ $this->setupService->createNewAccount(
+ self::ACCOUNT_NAME,
+ self::EMAIL_ADDRESS,
+ self::IMAP_HOST,
+ self::IMAP_PORT,
+ self::IMAP_SSL_MODE,
+ self::IMAP_USER,
+ self::IMAP_PASSWORD,
+ self::SMTP_HOST,
+ self::SMTP_PORT,
+ self::SMTP_SSL_MODE,
+ self::SMTP_USER,
+ self::SMTP_PASSWORD,
+ self::USER_ID,
+ 'invalid'
+ );
+ }
+
+ public function testCreateNewAccountImapConnectionFailure(): void {
+ $this->expectException(CouldNotConnectException::class);
+
+ $this->mockPasswordEncryption();
+
+ $imapClient = $this->createMock(Horde_Imap_Client_Socket::class);
+ $imapClient->expects(self::once())
+ ->method('login')
+ ->willThrowException(new Horde_Imap_Client_Exception('Connection failed'));
+ $imapClient->expects(self::once())
+ ->method('logout');
+
+ $this->imapClientFactory->expects(self::once())
+ ->method('getClient')
+ ->willReturn($imapClient);
+
+ $this->setupService->createNewAccount(
+ self::ACCOUNT_NAME,
+ self::EMAIL_ADDRESS,
+ self::IMAP_HOST,
+ self::IMAP_PORT,
+ self::IMAP_SSL_MODE,
+ self::IMAP_USER,
+ self::IMAP_PASSWORD,
+ self::SMTP_HOST,
+ self::SMTP_PORT,
+ self::SMTP_SSL_MODE,
+ self::SMTP_USER,
+ self::SMTP_PASSWORD,
+ self::USER_ID,
+ self::AUTH_METHOD_PASSWORD
+ );
+ }
+
+ public function testCreateNewAccountSmtpConnectionFailure(): void {
+ $this->expectException(CouldNotConnectException::class);
+
+ $this->mockPasswordEncryption();
+ $this->mockSuccessfulImapConnection();
+
+ $smtpTransport = $this->createMock(Horde_Mail_Transport_Smtphorde::class);
+ $smtpTransport->expects(self::once())
+ ->method('getSMTPObject')
+ ->willThrowException(new Horde_Mail_Exception('SMTP connection failed'));
+
+ $this->smtpClientFactory->expects(self::once())
+ ->method('create')
+ ->willReturn($smtpTransport);
+
+ $this->setupService->createNewAccount(
+ self::ACCOUNT_NAME,
+ self::EMAIL_ADDRESS,
+ self::IMAP_HOST,
+ self::IMAP_PORT,
+ self::IMAP_SSL_MODE,
+ self::IMAP_USER,
+ self::IMAP_PASSWORD,
+ self::SMTP_HOST,
+ self::SMTP_PORT,
+ self::SMTP_SSL_MODE,
+ self::SMTP_USER,
+ self::SMTP_PASSWORD,
+ self::USER_ID,
+ self::AUTH_METHOD_PASSWORD
+ );
+ }
+}