diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fb432f3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: 2026 STRATO GmbH +# SPDX-License-Identifier: AGPL-3.0-or-later + +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# PHP files +[*.php] +indent_style = tab +indent_size = 4 + +# JSON files +[*.json] +indent_style = space +indent_size = 2 + +# YAML files +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +# XML files +[*.xml] +indent_style = tab +indent_size = 4 + +# Markdown files +[*.md] +trim_trailing_whitespace = false + +# Shell scripts +[*.sh] +indent_style = tab +indent_size = 4 + +# Makefile +[Makefile] +indent_style = tab diff --git a/README.md b/README.md index 9bb359e..a692717 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,22 @@ NCW Tools enhances Nextcloud with advanced system utilities including event list php occ app:enable ncw_tools ``` +## Configuration + +### Post-Installation Job Retry Interval + +The app includes a background job that sends a welcome email to the admin user after installation. You can configure how often this job retries by setting the following system configuration value in your `config/config.php`: + +```php +'ncw_tools.post_setup_job.retry_interval' => 5, // Retry every 5 seconds (default: 2) +``` + +**Configuration Options:** +- **Key:** `ncw_tools.post_setup_job.retry_interval` +- **Type:** Integer (seconds) +- **Default:** `2` seconds +- **Description:** Interval in seconds between retry attempts for the post-installation welcome email job + ## Development ### Prerequisites diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index adb66b9..a8c6535 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -16,13 +16,9 @@ use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\Install\Events\InstallationCompletedEvent; -/** - * @psalm-suppress UnusedClass - */ class Application extends App implements IBootstrap { public const APP_ID = 'ncw_tools'; - /** @psalm-suppress PossiblyUnusedMethod */ public function __construct() { parent::__construct(self::APP_ID); } diff --git a/lib/BackgroundJob/PostSetupJob.php b/lib/BackgroundJob/PostSetupJob.php index abfdaf4..1f09855 100644 --- a/lib/BackgroundJob/PostSetupJob.php +++ b/lib/BackgroundJob/PostSetupJob.php @@ -23,9 +23,11 @@ use Psr\Log\LoggerInterface; class PostSetupJob extends TimedJob { - /** - * @psalm-suppress PossiblyUnusedMethod - Constructor called by DI container - */ + public const JOB_STATUS_INIT = 'INIT'; + public const JOB_STATUS_DONE = 'DONE'; + public const JOB_STATUS_UNKNOWN = 'UNKNOWN'; + public const JOB_STATUS_CONFIG_KEY = 'post_install'; + public function __construct( private LoggerInterface $logger, private IAppConfig $appConfig, @@ -37,65 +39,123 @@ public function __construct( private WelcomeMailHelper $welcomeMailHelper, ) { parent::__construct($timeFactory); - // Interval every 2 seconds - $this->setInterval(2); + $retryInterval = $this->config->getSystemValueInt('ncw_tools.post_setup_job.retry_interval', 2); + $this->setInterval($retryInterval); $this->setTimeSensitivity(IJob::TIME_SENSITIVE); + $this->logger->debug('PostSetupJob initialized', [ + 'retryInterval' => $retryInterval, + 'timeSensitivity' => 'TIME_SENSITIVE', + ]); } /** - * @param mixed $argument + * Execute the post-installation job + * + * Checks if the job has already completed and sends the initial welcome email + * to the admin user. The job will retry until successful or marked as done. + * + * @param mixed $argument The admin user ID as a string */ protected function run($argument): void { // string post install variable // used to check if job has already run - $jobStatus = $this->appConfig->getValueString(Application::APP_ID, 'post_install', 'UNKNOWN'); - if ($jobStatus === 'DONE') { - $this->logger->debug('Job was already successful, remove job from jobList'); + $jobStatus = $this->appConfig->getValueString(Application::APP_ID, self::JOB_STATUS_CONFIG_KEY, self::JOB_STATUS_UNKNOWN); + if ($jobStatus === self::JOB_STATUS_DONE) { + $this->logger->info('Post-installation job already completed, removing from queue'); $this->jobList->remove($this); return; } - if ($jobStatus === 'UNKNOWN') { - $this->logger->debug('Could not load job status from database, wait for another retry'); + if ($jobStatus === self::JOB_STATUS_UNKNOWN) { + $this->logger->warning('Job status unknown, waiting for initialization'); return; } - $this->logger->debug('Post install job started'); $initAdminId = (string)$argument; + $this->logger->info('Starting post-installation job', ['adminUserId' => $initAdminId]); $this->sendInitialWelcomeMail($initAdminId); - $this->logger->debug('Post install job finished'); + $this->logger->info('Post-installation job completed', ['adminUserId' => $initAdminId]); } + /** + * Send initial welcome email to the admin user + * + * Validates system URL configuration, checks URL availability, verifies user exists, + * and sends the welcome email with a password reset token. On success, marks the job + * as done and removes it from the queue. Failures are logged and will trigger a retry. + * + * @param string $adminUserId The admin user ID to send the welcome email to + */ protected function sendInitialWelcomeMail(string $adminUserId): void { - $client = $this->clientService->newClient(); $overwriteUrl = (string)$this->config->getSystemValue('overwrite.cli.url'); + + if (empty($overwriteUrl)) { + $this->logger->warning('System URL not configured, cannot send welcome email', [ + 'adminUserId' => $adminUserId, + 'config_key' => 'overwrite.cli.url', + ]); + return; + } + if (! $this->isUrlAvailable($client, $overwriteUrl)) { - $this->logger->debug('domain is not ready yet, retry with cron until ' . $overwriteUrl . ' is accessible'); + $this->logger->info('System not ready, will retry sending welcome email', [ + 'adminUserId' => $adminUserId, + 'url' => $overwriteUrl, + ]); return; } if (! $this->userManager->userExists($adminUserId)) { - $this->logger->warning('Could not find install user, skip sending welcome mail'); - } else { - $initAdminUser = $this->userManager->get($adminUserId); - if ($initAdminUser !== null) { - $this->welcomeMailHelper->sendWelcomeMail($initAdminUser, true); - } + $this->logger->warning('Admin user not found, cannot send welcome email', [ + 'adminUserId' => $adminUserId, + ]); + return; } - $this->appConfig->setValueString(Application::APP_ID, 'post_install', 'DONE'); + + $initAdminUser = $this->userManager->get($adminUserId); + if ($initAdminUser === null) { + $this->logger->error('Failed to retrieve admin user, will retry', [ + 'adminUserId' => $adminUserId, + ]); + return; + } + + try { + $this->welcomeMailHelper->sendWelcomeMail($initAdminUser, true); + } catch (\Throwable $e) { + $this->logger->error('Failed to send welcome email, will retry', [ + 'adminUserId' => $adminUserId, + 'exception' => $e->getMessage(), + ]); + return; + } + + $this->appConfig->setValueString(Application::APP_ID, self::JOB_STATUS_CONFIG_KEY, self::JOB_STATUS_DONE); $this->jobList->remove($this); } + /** + * Check if the system URL is accessible + * + * Performs an HTTP GET request to the status.php endpoint to verify the system + * is ready. Returns true if the response status code is in the 2xx range. + * + * @param IClient $client The HTTP client to use for the request + * @param string $baseUrl The base URL to check (e.g., https://example.com) + * @return bool True if the URL is accessible, false otherwise + */ private function isUrlAvailable(IClient $client, string $baseUrl): bool { - $url = $baseUrl . '/status.php'; try { - $this->logger->debug('Check URL: ' . $url); + $this->logger->debug('Checking URL availability', ['url' => $url]); $response = $client->get($url); - return $response->getStatusCode() >= 200 && $response->getStatusCode() < 300; - + $statusCode = $response->getStatusCode(); + return $statusCode >= 200 && $statusCode < 300; } catch (\Exception $ex) { - $this->logger->debug('Exception for ' . $url . '. Reason: ' . $ex->getMessage()); + $this->logger->info('URL not yet accessible', [ + 'url' => $url, + 'exception' => $ex->getMessage(), + ]); return false; } } diff --git a/lib/Helper/WelcomeMailHelper.php b/lib/Helper/WelcomeMailHelper.php index b318657..e9b0356 100644 --- a/lib/Helper/WelcomeMailHelper.php +++ b/lib/Helper/WelcomeMailHelper.php @@ -9,8 +9,8 @@ namespace OCA\NcwTools\Helper; -use OC\AppFramework\Utility\TimeFactory; use OCA\Settings\Mailer\NewUserMailHelper; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\Defaults; use OCP\IConfig; use OCP\IURLGenerator; @@ -22,9 +22,6 @@ use OCP\Util; class WelcomeMailHelper { - /** - * @psalm-suppress PossiblyUnusedMethod - Constructor called by DI container - */ public function __construct( private Defaults $defaults, private ICrypto $crypto, @@ -33,13 +30,19 @@ public function __construct( private IFactory $l10NFactory, private ISecureRandom $secureRandom, private IConfig $config, + private ITimeFactory $timeFactory, ) { } /** - * @psalm-suppress UndefinedClass - Using internal Nextcloud classes - * @psalm-suppress MixedAssignment - * @psalm-suppress MixedMethodCall + * Send a welcome email to a user with optional password reset token + * + * Creates a NewUserMailHelper instance and uses it to generate and send + * the welcome email template to the specified user. + * + * @param IUser $user The user to send the welcome email to + * @param bool $generatePasswordResetToken Whether to include a password reset token in the email + * @throws \Exception If email generation or sending fails */ public function sendWelcomeMail(IUser $user, bool $generatePasswordResetToken): void { $newUserMailHelper = new NewUserMailHelper( @@ -48,7 +51,7 @@ public function sendWelcomeMail(IUser $user, bool $generatePasswordResetToken): $this->l10NFactory, $this->mailer, $this->secureRandom, - new TimeFactory(), + $this->timeFactory, $this->config, $this->crypto, Util::getDefaultEmailAddress('no-reply') diff --git a/lib/Listeners/InstallationCompletedEventListener.php b/lib/Listeners/InstallationCompletedEventListener.php index 1bec805..0da1374 100644 --- a/lib/Listeners/InstallationCompletedEventListener.php +++ b/lib/Listeners/InstallationCompletedEventListener.php @@ -15,19 +15,14 @@ use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\IAppConfig; +use OCP\Install\Events\InstallationCompletedEvent; use Psr\Log\LoggerInterface; /** - * @template-implements IEventListener + * @template-implements IEventListener */ class InstallationCompletedEventListener implements IEventListener { - private string $adminConfigPath = '/vault/secrets/adminconfig'; - private array $quotesArray = ['\\\'', '"', '\'']; - - /** - * @psalm-suppress PossiblyUnusedMethod - Constructor called by DI container - */ public function __construct( private IAppConfig $appConfig, private LoggerInterface $logger, @@ -35,39 +30,29 @@ public function __construct( ) { } + /** + * Handle the InstallationCompletedEvent + * + * When installation completes, this listener schedules a PostSetupJob + * to send the initial welcome email to the admin user. + * + * @param Event $event The event to handle (must be InstallationCompletedEvent) + */ public function handle(Event $event): void { + if (!$event instanceof InstallationCompletedEvent) { + return; + } - $this->appConfig->setValueString(Application::APP_ID, 'post_install', 'INIT'); - - $this->logger->debug('post Setup: init admin user'); - $adminUserId = $this->initAdminUser(); - $this->logger->debug('post Setup: admin user configured'); - - $this->logger->debug('post Setup: add send initial welcome mail job'); - $this->jobList->add(PostSetupJob::class, $adminUserId); - $this->logger->debug('post Setup: job added'); - } - - - protected function initAdminUser(): string { - - // Read the configuration file line by line - $adminConfigLines = file($this->adminConfigPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - - // Initialize the associative array for the configuration - $adminConfig = []; + $this->appConfig->setValueString(Application::APP_ID, PostSetupJob::JOB_STATUS_CONFIG_KEY, PostSetupJob::JOB_STATUS_INIT); - // Iterate through the lines and extract the variables - foreach ($adminConfigLines as $line) { - $parts = explode('=', $line, 2); - if (count($parts) === 2) { - [$key, $value] = $parts; - $adminConfig[trim($key)] = trim($value); - } + $adminUserId = $event->getAdminUsername(); + if ($adminUserId === null) { + $this->logger->warning('No admin user provided in InstallationCompletedEvent'); + return; } - $adminUser = $adminConfig['NEXTCLOUD_ADMIN_USER'] ?? ''; - /** @psalm-suppress MixedArgumentTypeCoercion */ - return str_replace($this->quotesArray, '', $adminUser); + $this->logger->info('Scheduling welcome email job', ['adminUserId' => $adminUserId]); + $this->jobList->add(PostSetupJob::class, $adminUserId); + $this->logger->debug('Welcome email job scheduled successfully', ['adminUserId' => $adminUserId]); } } diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..5c064bb --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,2 @@ + + diff --git a/psalm.xml b/psalm.xml index 2840bda..6f28d5e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,22 +1,38 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php b/tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php new file mode 100644 index 0000000..f7d1c40 --- /dev/null +++ b/tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php @@ -0,0 +1,68 @@ +dataDirectory; + } + + /** + * Get the admin username if an admin user was created + * + * @since 33.0.0 + */ + public function getAdminUsername(): ?string { + return $this->adminUsername; + } + + /** + * Get the admin email if configured + * + * @since 33.0.0 + */ + public function getAdminEmail(): ?string { + return $this->adminEmail; + } + + /** + * Check if an admin user was created during installation + * + * @since 33.0.0 + */ + public function hasAdminUser(): bool { + return $this->adminUsername !== null; + } } diff --git a/tests/unit/BackgroundJob/PostSetupJobTest.php b/tests/unit/BackgroundJob/PostSetupJobTest.php index 9f1e8b3..dbb1ef6 100644 --- a/tests/unit/BackgroundJob/PostSetupJobTest.php +++ b/tests/unit/BackgroundJob/PostSetupJobTest.php @@ -47,6 +47,19 @@ protected function setUp(): void { $this->jobList = $this->createMock(IJobList::class); $this->welcomeMailHelper = $this->createMock(WelcomeMailHelper::class); + // Default config expectation for retry interval + $this->config->expects($this->any()) + ->method('getSystemValueInt') + ->with('ncw_tools.post_setup_job.retry_interval', 2) + ->willReturn(2); + + // Expect debug log in constructor + $this->logger->expects($this->any()) + ->method('debug') + ->willReturnCallback(function ($message, $context = []) { + // Allow any debug messages + }); + $this->job = new PostSetupJob( $this->logger, $this->appConfig, @@ -66,8 +79,8 @@ public function testRunWithJobAlreadyDone(): void { ->willReturn('DONE'); $this->logger->expects($this->once()) - ->method('debug') - ->with('Job was already successful, remove job from jobList'); + ->method('info') + ->with('Post-installation job already completed, removing from queue'); // Use reflection to call protected method $reflection = new \ReflectionClass($this->job); @@ -83,8 +96,8 @@ public function testRunWithUnknownStatus(): void { ->willReturn('UNKNOWN'); $this->logger->expects($this->once()) - ->method('debug') - ->with('Could not load job status from database, wait for another retry'); + ->method('warning') + ->with('Job status unknown, waiting for initialization'); // Use reflection to call protected method $reflection = new \ReflectionClass($this->job); @@ -124,22 +137,17 @@ public function testRunWithPendingStatus(): void { ->with('test-admin') ->willReturn(false); - // Expect 4 debug calls: - // 1. "Post install job started" - // 2. "Check URL: ..." - // 3. (conditional - only if URL check fails) "domain is not ready yet..." - // 4. "Post install job finished" + // Expect info logs for job start/completion and system ready $this->logger->expects($this->atLeastOnce()) - ->method('debug'); + ->method('info'); $this->logger->expects($this->once()) ->method('warning') - ->with('Could not find install user, skip sending welcome mail'); + ->with('Admin user not found, cannot send welcome email', $this->anything()); - // setValueString is called even when user doesn't exist - $this->appConfig->expects($this->once()) - ->method('setValueString') - ->with('ncw_tools', 'post_install', 'DONE'); + // setValueString should NOT be called when user doesn't exist (job will retry) + $this->appConfig->expects($this->never()) + ->method('setValueString'); // Use reflection to call protected method $reflection = new \ReflectionClass($this->job); @@ -169,13 +177,16 @@ public function testRunWithUrlNotAvailable(): void { ->method('newClient') ->willReturn($client); - // Expect 5 debug calls: - // 1. "Post install job started" - // 2. "Check URL: ..." - // 3. "Exception for..." - // 4. "domain is not ready yet..." - // 5. "Post install job finished" - $this->logger->expects($this->exactly(5)) + // Expect info calls for: + // 1. "Starting post-installation job" + // 2. "URL not yet accessible" (from exception) + // 3. "System not ready, will retry sending welcome email" + // 4. "Post-installation job completed" + $this->logger->expects($this->atLeastOnce()) + ->method('info'); + + // Expect 1 debug call for URL checking + $this->logger->expects($this->once()) ->method('debug'); $this->appConfig->expects($this->never()) @@ -248,4 +259,60 @@ public function testRunWithSuccessfulMailSend(): void { $method->setAccessible(true); $method->invoke($this->job, 'test-admin'); } + + public function testConstructorUsesDefaultRetryInterval(): void { + $logger = $this->createMock(LoggerInterface::class); + $config = $this->createMock(IConfig::class); + + $config->expects($this->once()) + ->method('getSystemValueInt') + ->with('ncw_tools.post_setup_job.retry_interval', 2) + ->willReturn(2); + + $logger->expects($this->once()) + ->method('debug') + ->with('PostSetupJob initialized', [ + 'retryInterval' => 2, + 'timeSensitivity' => 'TIME_SENSITIVE', + ]); + + new PostSetupJob( + $logger, + $this->appConfig, + $config, + $this->userManager, + $this->clientService, + $this->timeFactory, + $this->jobList, + $this->welcomeMailHelper + ); + } + + public function testConstructorUsesCustomRetryInterval(): void { + $logger = $this->createMock(LoggerInterface::class); + $config = $this->createMock(IConfig::class); + + $config->expects($this->once()) + ->method('getSystemValueInt') + ->with('ncw_tools.post_setup_job.retry_interval', 2) + ->willReturn(10); + + $logger->expects($this->once()) + ->method('debug') + ->with('PostSetupJob initialized', [ + 'retryInterval' => 10, + 'timeSensitivity' => 'TIME_SENSITIVE', + ]); + + new PostSetupJob( + $logger, + $this->appConfig, + $config, + $this->userManager, + $this->clientService, + $this->timeFactory, + $this->jobList, + $this->welcomeMailHelper + ); + } } diff --git a/tests/unit/Helper/WelcomeMailHelperTest.php b/tests/unit/Helper/WelcomeMailHelperTest.php index 1695ba4..ceae501 100644 --- a/tests/unit/Helper/WelcomeMailHelperTest.php +++ b/tests/unit/Helper/WelcomeMailHelperTest.php @@ -10,6 +10,7 @@ namespace OCA\NcwTools\Tests\Unit\Helper; use OCA\NcwTools\Helper\WelcomeMailHelper; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\Defaults; use OCP\IConfig; use OCP\IL10N; @@ -32,6 +33,7 @@ class WelcomeMailHelperTest extends TestCase { private IFactory&MockObject $l10NFactory; private ISecureRandom&MockObject $secureRandom; private IConfig&MockObject $config; + private ITimeFactory&MockObject $timeFactory; private WelcomeMailHelper $welcomeMailHelper; protected function setUp(): void { @@ -44,52 +46,40 @@ protected function setUp(): void { $this->l10NFactory = $this->createMock(IFactory::class); $this->secureRandom = $this->createMock(ISecureRandom::class); $this->config = $this->createMock(IConfig::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); - // Mock IL10N for l10NFactory + // Setup mocks required by NewUserMailHelper $l10n = $this->createMock(IL10N::class); - $l10n->method('t')->willReturnCallback(function ($text) { - return $text; - }); + $l10n->method('t')->willReturnArgument(0); $this->l10NFactory->method('get')->willReturn($l10n); + $this->l10NFactory->method('getUserLanguage')->willReturn('en'); - // Mock email template $emailTemplate = $this->createMock(IEMailTemplate::class); $emailTemplate->method('addHeader')->willReturnSelf(); $emailTemplate->method('addHeading')->willReturnSelf(); $emailTemplate->method('addBodyText')->willReturnSelf(); $emailTemplate->method('addBodyButton')->willReturnSelf(); - $emailTemplate->method('addBodyButtonGroup')->willReturnCallback(function () use ($emailTemplate) { - return $emailTemplate; - }); + $emailTemplate->method('addBodyButtonGroup')->willReturnSelf(); $emailTemplate->method('addFooter')->willReturnSelf(); $emailTemplate->method('setSubject')->willReturnSelf(); $this->mailer->method('createEMailTemplate')->willReturn($emailTemplate); - // Mock IConfig to return a client download URL - $this->config->method('getSystemValue')->willReturnCallback(function ($key, $default) { - if ($key === 'customclient_desktop') { - return 'https://nextcloud.com/install/#install-clients'; - } - return $default; - }); - - // Mock message $message = $this->createMock(IMessage::class); $message->method('setTo')->willReturnSelf(); $message->method('setFrom')->willReturnSelf(); $message->method('useTemplate')->willReturnSelf(); + $message->method('setAutoSubmitted')->willReturnSelf(); $this->mailer->method('createMessage')->willReturn($message); - // Mock defaults $this->defaults->method('getName')->willReturn('Nextcloud'); - - // Mock URL generator $this->urlGenerator->method('getAbsoluteURL')->willReturn('https://example.com'); $this->urlGenerator->method('linkToRouteAbsolute')->willReturn('https://example.com/reset'); - - // Mock secure random and crypto - $this->secureRandom->method('generate')->willReturn('random-token'); + $this->secureRandom->method('generate')->willReturn('test-token'); $this->crypto->method('encrypt')->willReturn('encrypted-token'); + $this->config->method('getSystemValue')->willReturnMap([ + ['customclient_desktop', 'https://nextcloud.com/install/#install-clients', 'https://nextcloud.com/install/#install-clients'], + ['secret', '', 'test-secret'], + ]); $this->welcomeMailHelper = new WelcomeMailHelper( $this->defaults, @@ -98,44 +88,62 @@ protected function setUp(): void { $this->urlGenerator, $this->l10NFactory, $this->secureRandom, - $this->config + $this->config, + $this->timeFactory ); } public function testSendWelcomeMailWithPasswordResetToken(): void { $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('testuser'); - $user->expects($this->atLeastOnce()) - ->method('getEMailAddress') - ->willReturn('testuser@example.com'); - - // The method should not throw any exceptions - try { - $this->welcomeMailHelper->sendWelcomeMail($user, true); - $this->assertTrue(true); // Explicit assertion that we got here without exception - } catch (\Exception $e) { - $this->fail('sendWelcomeMail should not throw exceptions: ' . $e->getMessage()); - } + $user->method('getUID')->willReturn('testuser'); + $user->method('getDisplayName')->willReturn('Test User'); + $user->method('getEMailAddress')->willReturn('testuser@example.com'); + $user->method('getBackendClassName')->willReturn('Database'); + + $this->timeFactory->expects($this->once()) + ->method('getTime') + ->willReturn(1234567890); + + $this->config->expects($this->once()) + ->method('setUserValue') + ->with('testuser', 'core', 'lostpassword', $this->anything()); + + $this->mailer->expects($this->once()) + ->method('send'); + + $this->welcomeMailHelper->sendWelcomeMail($user, true); } public function testSendWelcomeMailWithoutPasswordResetToken(): void { $user = $this->createMock(IUser::class); - $user->expects($this->atLeastOnce()) - ->method('getUID') - ->willReturn('testuser'); - $user->expects($this->atLeastOnce()) - ->method('getEMailAddress') - ->willReturn('testuser@example.com'); - - // The method should not throw any exceptions - try { - $this->welcomeMailHelper->sendWelcomeMail($user, false); - $this->assertTrue(true); // Explicit assertion that we got here without exception - } catch (\Exception $e) { - $this->fail('sendWelcomeMail should not throw exceptions: ' . $e->getMessage()); - } + $user->method('getUID')->willReturn('testuser'); + $user->method('getDisplayName')->willReturn('Test User'); + $user->method('getEMailAddress')->willReturn('testuser@example.com'); + $user->method('getBackendClassName')->willReturn('Database'); + + $this->timeFactory->expects($this->never()) + ->method('getTime'); + + $this->config->expects($this->never()) + ->method('setUserValue'); + + $this->mailer->expects($this->once()) + ->method('send'); + + $this->welcomeMailHelper->sendWelcomeMail($user, false); + } + + public function testSendWelcomeMailWithUserWithoutEmail(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + $user->method('getDisplayName')->willReturn('Test User'); + $user->method('getEMailAddress')->willReturn(null); + $user->method('getBackendClassName')->willReturn('Database'); + + $this->mailer->expects($this->never()) + ->method('send'); + + $this->welcomeMailHelper->sendWelcomeMail($user, false); } public function testConstructorInitializesCorrectly(): void { @@ -147,6 +155,7 @@ public function testConstructorInitializesCorrectly(): void { $this->l10NFactory, $this->secureRandom, $this->config, + $this->timeFactory ); $this->assertInstanceOf(WelcomeMailHelper::class, $helper); diff --git a/tests/unit/Listeners/InstallationCompletedEventListenerTest.php b/tests/unit/Listeners/InstallationCompletedEventListenerTest.php index 1c9387e..84bca29 100644 --- a/tests/unit/Listeners/InstallationCompletedEventListenerTest.php +++ b/tests/unit/Listeners/InstallationCompletedEventListenerTest.php @@ -14,6 +14,7 @@ use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\Event; use OCP\IAppConfig; +use OCP\Install\Events\InstallationCompletedEvent; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Test\TestCase; @@ -24,8 +25,6 @@ class InstallationCompletedEventListenerTest extends TestCase { private IJobList&MockObject $jobList; private InstallationCompletedEventListener $listener; - private string $testAdminConfigPath; - protected function setUp(): void { parent::setUp(); @@ -33,45 +32,25 @@ protected function setUp(): void { $this->logger = $this->createMock(LoggerInterface::class); $this->jobList = $this->createMock(IJobList::class); - // Create temporary test config file - $this->testAdminConfigPath = sys_get_temp_dir() . '/test_adminconfig_' . uniqid(); - $this->listener = new InstallationCompletedEventListener( $this->appConfig, $this->logger, $this->jobList ); - - // Use reflection to override config path for testing - $adminPathProperty = new \ReflectionProperty(InstallationCompletedEventListener::class, 'adminConfigPath'); - $adminPathProperty->setAccessible(true); - $adminPathProperty->setValue($this->listener, $this->testAdminConfigPath); - } - - protected function tearDown(): void { - // Clean up temporary file - if (file_exists($this->testAdminConfigPath)) { - unlink($this->testAdminConfigPath); - } - - parent::tearDown(); } public function testHandleSetsAppConfigAndAddsJob(): void { - // Create test config file - $adminConfig = <<<'EOT' -NEXTCLOUD_ADMIN_USER=admin -EOT; - file_put_contents($this->testAdminConfigPath, $adminConfig); - - $event = $this->createMock(Event::class); + $event = new InstallationCompletedEvent( + '/var/www/html/data', + 'admin', + 'admin@example.com' + ); // Expect app config to be set for 'post_install' $this->appConfig->expects($this->once()) ->method('setValueString') ->with('ncw_tools', 'post_install', 'INIT'); - // Expect job to be added $this->jobList->expects($this->once()) ->method('add') @@ -84,24 +63,36 @@ public function testHandleSetsAppConfigAndAddsJob(): void { $this->listener->handle($event); } - public function testHandleWithQuotedValues(): void { - // Create test config file with quoted values - $adminConfig = <<<'EOT' -NEXTCLOUD_ADMIN_USER="admin" -EOT; - file_put_contents($this->testAdminConfigPath, $adminConfig); - - $event = $this->createMock(Event::class); + public function testHandleWithoutAdminUser(): void { + $event = new InstallationCompletedEvent( + '/var/www/html/data' + ); $this->appConfig->expects($this->once()) ->method('setValueString') ->with('ncw_tools', 'post_install', 'INIT'); - // Expect job to be added with admin user (quotes should be stripped) - $this->jobList->expects($this->once()) - ->method('add') - ->with(PostSetupJob::class, 'admin'); + // Expect warning when no admin user + $this->logger->expects($this->once()) + ->method('warning') + ->with('No admin user provided in InstallationCompletedEvent'); + + // Job should NOT be added + $this->jobList->expects($this->never()) + ->method('add'); + + $this->listener->handle($event); + } + + public function testHandleWithWrongEventType(): void { + $event = $this->createMock(Event::class); + + // Should not process non-InstallationCompletedEvent events + $this->appConfig->expects($this->never()) + ->method('setValueString'); + $this->jobList->expects($this->never()) + ->method('add'); $this->listener->handle($event); }