diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index b717d1c2d0..cac4ef0559 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -22,6 +22,7 @@ use OCA\Mail\Exception\ClientException; use OCA\Mail\Exception\ServiceException; use OCA\Mail\IMAP\IMAPClientFactory; +use OCA\Mail\Service\IONOS\IonosAccountDeletionService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJob; @@ -56,6 +57,7 @@ public function __construct( IMAPClientFactory $imapClientFactory, private readonly IConfig $config, private readonly ITimeFactory $timeFactory, + private readonly IonosAccountDeletionService $ionosAccountDeletionService, ) { $this->mapper = $mapper; $this->aliasesService = $aliasesService; @@ -151,6 +153,7 @@ public function delete(string $currentUserId, int $accountId): void { } catch (DoesNotExistException $e) { throw new ClientException("Account $accountId does not exist", 0, $e); } + $this->ionosAccountDeletionService->handleMailAccountDeletion($mailAccount); $this->aliasesService->deleteAll($accountId); $this->mapper->delete($mailAccount); } @@ -166,6 +169,7 @@ public function deleteByAccountId(int $accountId): void { } catch (DoesNotExistException $e) { throw new ClientException("Account $accountId does not exist", 0, $e); } + $this->ionosAccountDeletionService->handleMailAccountDeletion($mailAccount); $this->aliasesService->deleteAll($accountId); $this->mapper->delete($mailAccount); } diff --git a/lib/Service/IONOS/IonosAccountDeletionService.php b/lib/Service/IONOS/IonosAccountDeletionService.php new file mode 100644 index 0000000000..d81c209ce4 --- /dev/null +++ b/lib/Service/IONOS/IonosAccountDeletionService.php @@ -0,0 +1,136 @@ +ionosConfigService->isIonosIntegrationEnabled()) { + return; + } + + try { + if (!$this->shouldDeleteIonosMailbox($mailAccount)) { + return; + } + + $this->logger->info('Detected IONOS mail account deletion, attempting to delete IONOS mailbox', [ + 'email' => $mailAccount->getEmail(), + 'userId' => $mailAccount->getUserId(), + 'accountId' => $mailAccount->getId(), + ]); + + // Use tryDeleteEmailAccount to avoid throwing exceptions + $this->ionosMailService->tryDeleteEmailAccount($mailAccount->getUserId()); + } catch (\Exception $e) { + // Log but don't throw - account deletion in Nextcloud should proceed + $this->logger->error('Error checking/deleting IONOS mailbox during account deletion', [ + 'exception' => $e, + 'accountId' => $mailAccount->getId(), + ]); + } + } + + /** + * Check if the mail account is an IONOS-managed account that should be deleted + * + * @param MailAccount $mailAccount The mail account to check + * @return bool True if this is an IONOS-managed account that should be deleted + */ + private function shouldDeleteIonosMailbox(MailAccount $mailAccount): bool { + $email = $mailAccount->getEmail(); + $userId = $mailAccount->getUserId(); + $accountId = $mailAccount->getId(); + $ionosMailDomain = $this->ionosConfigService->getMailDomain(); + + // Check if the account's email domain matches the IONOS mail domain + if (empty($ionosMailDomain) || !$this->isIonosEmail($email, $ionosMailDomain)) { + return false; + } + + // Get the IONOS provisioned email for this user + $ionosProvisionedEmail = $this->ionosMailService->getIonosEmailForUser($userId); + + // If no IONOS account exists for this user, skip deletion + if ($ionosProvisionedEmail === null) { + $this->logger->debug('No IONOS provisioned account found for user, skipping deletion', [ + 'email' => $email, + 'userId' => $userId, + 'accountId' => $accountId, + ]); + return false; + } + + // Verify that the account being deleted matches the IONOS provisioned email + if (strcasecmp($email, $ionosProvisionedEmail) !== 0) { + $this->logger->warning('Mail account email does not match IONOS provisioned email, skipping deletion', [ + 'accountEmail' => $email, + 'ionosEmail' => $ionosProvisionedEmail, + 'userId' => $userId, + 'accountId' => $accountId, + ]); + return false; + } + + return true; + } + + /** + * Check if an email address belongs to the IONOS mail domain + * + * @param string $email The email address to check + * @param string $ionosMailDomain The IONOS mail domain + * @return bool True if the email belongs to the IONOS domain + */ + private function isIonosEmail(string $email, string $ionosMailDomain): bool { + if (empty($email) || empty($ionosMailDomain)) { + return false; + } + + // Extract domain from email address + $atPosition = strrpos($email, '@'); + if ($atPosition === false) { + return false; + } + + $emailDomain = substr($email, $atPosition + 1); + if ($emailDomain === '') { + return false; + } + + return strcasecmp($emailDomain, $ionosMailDomain) === 0; + } +} diff --git a/lib/Service/IONOS/IonosMailService.php b/lib/Service/IONOS/IonosMailService.php index e4b875588f..59279b6ee9 100644 --- a/lib/Service/IONOS/IonosMailService.php +++ b/lib/Service/IONOS/IonosMailService.php @@ -54,25 +54,44 @@ public function mailAccountExistsForCurrentUser(): bool { * @return bool true if account exists, false otherwise */ public function mailAccountExistsForCurrentUserId(string $userId): bool { + $response = $this->getMailAccountResponse($userId); + + if ($response !== null) { + $this->logger->debug('User has existing IONOS mail account', [ + 'email' => $response->getEmail(), + 'userId' => $userId + ]); + return true; + } + + return false; + } + + /** + * Get the IONOS mail account response for a specific user + * + * @param string $userId The Nextcloud user ID + * @return MailAccountResponse|null The account response if it exists, null otherwise + */ + private function getMailAccountResponse(string $userId): ?MailAccountResponse { try { - $this->logger->debug('Checking if user has email account', [ + $this->logger->debug('Getting IONOS mail account for user', [ 'userId' => $userId, 'extRef' => $this->configService->getExternalReference(), ]); $apiInstance = $this->createApiInstance(); - - $result = $apiInstance->getFunctionalAccount(self::BRAND, $this->configService->getExternalReference(), $userId); + $result = $apiInstance->getFunctionalAccount( + self::BRAND, + $this->configService->getExternalReference(), + $userId + ); if ($result instanceof MailAccountResponse) { - $this->logger->debug('User has existing IONOS mail account', [ - 'email' => $result->getEmail(), - 'userId' => $userId - ]); - return true; + return $result; } - return false; + return null; } catch (ApiException $e) { // 404 - no account exists if ($e->getCode() === self::HTTP_NOT_FOUND) { @@ -80,21 +99,22 @@ public function mailAccountExistsForCurrentUserId(string $userId): bool { 'userId' => $userId, 'statusCode' => $e->getCode() ]); - return false; + return null; } - $this->logger->error('API Exception when checking for existing mail account', [ + $this->logger->error('API Exception when getting IONOS mail account', [ 'statusCode' => $e->getCode(), 'message' => $e->getMessage(), - 'responseBody' => $e->getResponseBody() + 'responseBody' => $e->getResponseBody(), + 'userId' => $userId ]); - return false; + return null; } catch (\Exception $e) { - $this->logger->error('Exception when checking for existing mail account', [ + $this->logger->error('Exception when getting IONOS mail account', [ 'exception' => $e, 'userId' => $userId ]); - return false; + return null; } } @@ -318,6 +338,27 @@ public function deleteEmailAccount(string $userId): bool { } } + /** + * Get the email address of the IONOS account for a specific user + * + * @param string $userId The Nextcloud user ID + * @return string|null The email address if account exists, null otherwise + */ + public function getIonosEmailForUser(string $userId): ?string { + $response = $this->getMailAccountResponse($userId); + + if ($response !== null) { + $email = $response->getEmail(); + $this->logger->debug('Found IONOS mail account for user', [ + 'email' => $email, + 'userId' => $userId + ]); + return $email; + } + + return null; + } + /** * Delete an IONOS email account without throwing exceptions (fire and forget) * diff --git a/tests/Unit/Service/AccountServiceTest.php b/tests/Unit/Service/AccountServiceTest.php index d9c11bdeb5..2cf25638b4 100644 --- a/tests/Unit/Service/AccountServiceTest.php +++ b/tests/Unit/Service/AccountServiceTest.php @@ -19,6 +19,7 @@ use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AliasesService; +use OCA\Mail\Service\IONOS\IonosAccountDeletionService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; use OCP\IConfig; @@ -61,6 +62,7 @@ class AccountServiceTest extends TestCase { private IConfig&MockObject $config; private ITimeFactory&MockObject $time; + private IonosAccountDeletionService&MockObject $ionosAccountDeletionService; protected function setUp(): void { parent::setUp(); @@ -72,6 +74,7 @@ protected function setUp(): void { $this->imapClientFactory = $this->createMock(IMAPClientFactory::class); $this->config = $this->createMock(IConfig::class); $this->time = $this->createMock(ITimeFactory::class); + $this->ionosAccountDeletionService = $this->createMock(IonosAccountDeletionService::class); $this->accountService = new AccountService( $this->mapper, $this->aliasesService, @@ -79,6 +82,7 @@ protected function setUp(): void { $this->imapClientFactory, $this->config, $this->time, + $this->ionosAccountDeletionService, ); $this->account1 = new MailAccount(); @@ -139,6 +143,10 @@ public function testFindById() { public function testDelete() { $accountId = 33; + $this->ionosAccountDeletionService->expects($this->once()) + ->method('handleMailAccountDeletion') + ->with($this->account1); + $this->mapper->expects($this->once()) ->method('find') ->with($this->user, $accountId) @@ -153,6 +161,10 @@ public function testDelete() { public function testDeleteByAccountId() { $accountId = 33; + $this->ionosAccountDeletionService->expects($this->once()) + ->method('handleMailAccountDeletion') + ->with($this->account1); + $this->mapper->expects($this->once()) ->method('findById') ->with($accountId) diff --git a/tests/Unit/Service/IONOS/IonosAccountDeletionServiceTest.php b/tests/Unit/Service/IONOS/IonosAccountDeletionServiceTest.php new file mode 100644 index 0000000000..3905180e79 --- /dev/null +++ b/tests/Unit/Service/IONOS/IonosAccountDeletionServiceTest.php @@ -0,0 +1,267 @@ +ionosMailService = $this->createMock(IonosMailService::class); + $this->ionosConfigService = $this->createMock(IonosConfigService::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->service = new IonosAccountDeletionService( + $this->ionosMailService, + $this->ionosConfigService, + $this->logger, + ); + } + + public function testHandleMailAccountDeletionWhenIonosIntegrationDisabled(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(33); + $mailAccount->setUserId('testuser'); + $mailAccount->setEmail('testuser@example.com'); + + $this->ionosConfigService->expects($this->once()) + ->method('isIonosIntegrationEnabled') + ->willReturn(false); + + // IONOS mailbox deletion should NOT be attempted when integration is disabled + $this->ionosMailService->expects($this->never()) + ->method('tryDeleteEmailAccount'); + + $this->service->handleMailAccountDeletion($mailAccount); + } + + public function testHandleMailAccountDeletionForIonosAccount(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(33); + $mailAccount->setUserId('testuser'); + $mailAccount->setEmail('testuser@example.com'); + + $this->ionosConfigService->expects($this->once()) + ->method('isIonosIntegrationEnabled') + ->willReturn(true); + $this->ionosConfigService->expects($this->once()) + ->method('getMailDomain') + ->willReturn('example.com'); + + // Should check the IONOS provisioned email + $this->ionosMailService->expects($this->once()) + ->method('getIonosEmailForUser') + ->with('testuser') + ->willReturn('testuser@example.com'); + + $this->logger->expects($this->once()) + ->method('info') + ->with( + 'Detected IONOS mail account deletion, attempting to delete IONOS mailbox', + [ + 'email' => 'testuser@example.com', + 'userId' => 'testuser', + 'accountId' => 33, + ] + ); + + $this->ionosMailService->expects($this->once()) + ->method('tryDeleteEmailAccount') + ->with('testuser'); + + $this->service->handleMailAccountDeletion($mailAccount); + } + + public function testHandleMailAccountDeletionForNonIonosAccount(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(33); + $mailAccount->setUserId('testuser'); + $mailAccount->setEmail('testuser@otherdomain.com'); + + $this->ionosConfigService->expects($this->once()) + ->method('isIonosIntegrationEnabled') + ->willReturn(true); + $this->ionosConfigService->expects($this->once()) + ->method('getMailDomain') + ->willReturn('example.com'); + + // Should not check IONOS email for non-IONOS domain + $this->ionosMailService->expects($this->never()) + ->method('getIonosEmailForUser'); + + // IONOS mailbox deletion should NOT be called for non-IONOS domain + $this->ionosMailService->expects($this->never()) + ->method('tryDeleteEmailAccount'); + + $this->service->handleMailAccountDeletion($mailAccount); + } + + public function testHandleMailAccountDeletionWithEmptyDomain(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(33); + $mailAccount->setUserId('testuser'); + $mailAccount->setEmail('testuser@example.com'); + + $this->ionosConfigService->expects($this->once()) + ->method('isIonosIntegrationEnabled') + ->willReturn(true); + $this->ionosConfigService->expects($this->once()) + ->method('getMailDomain') + ->willReturn(''); + + // IONOS mailbox deletion should NOT be called when domain is empty + $this->ionosMailService->expects($this->never()) + ->method('tryDeleteEmailAccount'); + + $this->service->handleMailAccountDeletion($mailAccount); + } + + public function testHandleMailAccountDeletionWithException(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(33); + $mailAccount->setUserId('testuser'); + $mailAccount->setEmail('testuser@example.com'); + + $this->ionosConfigService->expects($this->once()) + ->method('isIonosIntegrationEnabled') + ->willReturn(true); + $this->ionosConfigService->expects($this->once()) + ->method('getMailDomain') + ->willThrowException(new \RuntimeException('Test exception')); + + // Exception should be caught and logged + $this->logger->expects($this->once()) + ->method('error') + ->with( + 'Error checking/deleting IONOS mailbox during account deletion', + $this->callback(function ($context) { + return isset($context['exception']) + && $context['exception'] instanceof \RuntimeException + && $context['accountId'] === 33; + }) + ); + + // Should not throw exception + $this->service->handleMailAccountDeletion($mailAccount); + } + + public function testHandleMailAccountDeletionCaseInsensitiveDomain(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(33); + $mailAccount->setUserId('testuser'); + $mailAccount->setEmail('testuser@EXAMPLE.COM'); + + $this->ionosConfigService->expects($this->once()) + ->method('isIonosIntegrationEnabled') + ->willReturn(true); + $this->ionosConfigService->expects($this->once()) + ->method('getMailDomain') + ->willReturn('example.com'); + + // Should check the IONOS provisioned email (case-insensitive match) + $this->ionosMailService->expects($this->once()) + ->method('getIonosEmailForUser') + ->with('testuser') + ->willReturn('testuser@example.com'); + + $this->ionosMailService->expects($this->once()) + ->method('tryDeleteEmailAccount') + ->with('testuser'); + + $this->service->handleMailAccountDeletion($mailAccount); + } + + public function testHandleMailAccountDeletionWhenNoIonosAccountProvisioned(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(33); + $mailAccount->setUserId('testuser'); + $mailAccount->setEmail('testuser@example.com'); + + $this->ionosConfigService->expects($this->once()) + ->method('isIonosIntegrationEnabled') + ->willReturn(true); + $this->ionosConfigService->expects($this->once()) + ->method('getMailDomain') + ->willReturn('example.com'); + + // User manually configured an account with IONOS domain but no provisioned account exists + $this->ionosMailService->expects($this->once()) + ->method('getIonosEmailForUser') + ->with('testuser') + ->willReturn(null); + + $this->logger->expects($this->once()) + ->method('debug') + ->with( + 'No IONOS provisioned account found for user, skipping deletion', + [ + 'email' => 'testuser@example.com', + 'userId' => 'testuser', + 'accountId' => 33, + ] + ); + + // IONOS mailbox deletion should NOT be called when no provisioned account exists + $this->ionosMailService->expects($this->never()) + ->method('tryDeleteEmailAccount'); + + $this->service->handleMailAccountDeletion($mailAccount); + } + + public function testHandleMailAccountDeletionWhenEmailMismatch(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(33); + $mailAccount->setUserId('testuser'); + $mailAccount->setEmail('different@example.com'); + + $this->ionosConfigService->expects($this->once()) + ->method('isIonosIntegrationEnabled') + ->willReturn(true); + $this->ionosConfigService->expects($this->once()) + ->method('getMailDomain') + ->willReturn('example.com'); + + // User has a provisioned IONOS account but is deleting a different account with the same domain + $this->ionosMailService->expects($this->once()) + ->method('getIonosEmailForUser') + ->with('testuser') + ->willReturn('testuser@example.com'); + + $this->logger->expects($this->once()) + ->method('warning') + ->with( + 'Mail account email does not match IONOS provisioned email, skipping deletion', + [ + 'accountEmail' => 'different@example.com', + 'ionosEmail' => 'testuser@example.com', + 'userId' => 'testuser', + 'accountId' => 33, + ] + ); + + // IONOS mailbox deletion should NOT be called when email doesn't match + $this->ionosMailService->expects($this->never()) + ->method('tryDeleteEmailAccount'); + + $this->service->handleMailAccountDeletion($mailAccount); + } +} diff --git a/tests/Unit/Service/IONOS/IonosMailServiceTest.php b/tests/Unit/Service/IONOS/IonosMailServiceTest.php index 1594b8318b..75ba54a88d 100644 --- a/tests/Unit/Service/IONOS/IonosMailServiceTest.php +++ b/tests/Unit/Service/IONOS/IonosMailServiceTest.php @@ -550,7 +550,7 @@ public function testMailAccountExistsForCurrentUserReturnsFalseOnApiError(): voi $this->logger->expects($this->once()) ->method('error') - ->with('API Exception when checking for existing mail account', $this->callback(function ($context) { + ->with('API Exception when getting IONOS mail account', $this->callback(function ($context) { return $context['statusCode'] === 500 && $context['message'] === 'Internal Server Error'; })); @@ -591,7 +591,7 @@ public function testMailAccountExistsForCurrentUserReturnsFalseOnGeneralExceptio $this->logger->expects($this->once()) ->method('error') - ->with('Exception when checking for existing mail account', $this->callback(function ($context) { + ->with('Exception when getting IONOS mail account', $this->callback(function ($context) { return isset($context['exception']) && $context['userId'] === 'testuser123'; })); @@ -1037,4 +1037,163 @@ public function testTryDeleteEmailAccountWhenMailboxNotFound(): void { $this->addToAssertionCount(1); } + + public function testGetIonosEmailForUserReturnsEmailWhenAccountExists(): void { + $userId = 'testuser123'; + $expectedEmail = 'testuser@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'); + + // 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($expectedEmail); + + $apiInstance->method('getFunctionalAccount') + ->with('IONOS', 'test-ext-ref', $userId) + ->willReturn($mailAccountResponse); + + // Expect logging calls + $this->logger->expects($this->exactly(2)) + ->method('debug'); + + $result = $this->service->getIonosEmailForUser($userId); + + $this->assertEquals($expectedEmail, $result); + } + + public function testGetIonosEmailForUserReturnsNullWhen404(): 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')->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, + [], + '{"error": "Not Found"}' + ); + + $apiInstance->method('getFunctionalAccount') + ->with('IONOS', 'test-ext-ref', $userId) + ->willThrowException($apiException); + + // Expect logging calls + $this->logger->expects($this->exactly(2)) + ->method('debug'); + + $result = $this->service->getIonosEmailForUser($userId); + + $this->assertNull($result); + } + + public function testGetIonosEmailForUserReturnsNullOnApiError(): 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')->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, + [], + '{"error": "Server error"}' + ); + + $apiInstance->method('getFunctionalAccount') + ->with('IONOS', 'test-ext-ref', $userId) + ->willThrowException($apiException); + + // Expect logging calls + $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($userId); + + $this->assertNull($result); + } + + public function testGetIonosEmailForUserReturnsNullOnGeneralException(): 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')->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')); + + // Expect logging calls + $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'; + })); + + $result = $this->service->getIonosEmailForUser($userId); + + $this->assertNull($result); + } }