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/composer.json b/composer.json index 57525de..31a83f0 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,11 @@ "OCA\\NcwTools\\": "lib/" } }, + "autoload-dev": { + "classmap": [ + "tests/stubs/" + ] + }, "scripts": { "post-install-cmd": [ "@composer bin all install --ansi" diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 28290cf..a8c6535 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -9,23 +9,22 @@ namespace OCA\NcwTools\AppInfo; +use OCA\NcwTools\Listeners\InstallationCompletedEventListener; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; 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); } public function register(IRegistrationContext $context): void { + $context->registerEventListener(InstallationCompletedEvent::class, InstallationCompletedEventListener::class); } public function boot(IBootContext $context): void { diff --git a/lib/BackgroundJob/PostSetupJob.php b/lib/BackgroundJob/PostSetupJob.php new file mode 100644 index 0000000..1f09855 --- /dev/null +++ b/lib/BackgroundJob/PostSetupJob.php @@ -0,0 +1,162 @@ +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', + ]); + } + + /** + * 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, 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 === self::JOB_STATUS_UNKNOWN) { + $this->logger->warning('Job status unknown, waiting for initialization'); + return; + } + + $initAdminId = (string)$argument; + $this->logger->info('Starting post-installation job', ['adminUserId' => $initAdminId]); + $this->sendInitialWelcomeMail($initAdminId); + $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->info('System not ready, will retry sending welcome email', [ + 'adminUserId' => $adminUserId, + 'url' => $overwriteUrl, + ]); + return; + } + if (! $this->userManager->userExists($adminUserId)) { + $this->logger->warning('Admin user not found, cannot send welcome email', [ + 'adminUserId' => $adminUserId, + ]); + return; + } + + $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('Checking URL availability', ['url' => $url]); + $response = $client->get($url); + $statusCode = $response->getStatusCode(); + return $statusCode >= 200 && $statusCode < 300; + } catch (\Exception $ex) { + $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 new file mode 100644 index 0000000..0d23031 --- /dev/null +++ b/lib/Helper/WelcomeMailHelper.php @@ -0,0 +1,67 @@ +defaults, + $this->urlGenerator, + $this->l10NFactory, + $this->mailer, + $this->secureRandom, + $this->timeFactory, + $this->config, + $this->crypto, + Util::getDefaultEmailAddress('no-reply') + ); + + $mailTmpl = $newUserMailHelper->generateTemplate($user, $generatePasswordResetToken); + if ($mailTmpl === null) { + // User has no email address, cannot send welcome mail + return; + } + $newUserMailHelper->sendMail($user, $mailTmpl); + } +} diff --git a/lib/Listeners/InstallationCompletedEventListener.php b/lib/Listeners/InstallationCompletedEventListener.php new file mode 100644 index 0000000..0da1374 --- /dev/null +++ b/lib/Listeners/InstallationCompletedEventListener.php @@ -0,0 +1,58 @@ + + */ +class InstallationCompletedEventListener implements IEventListener { + + public function __construct( + private IAppConfig $appConfig, + private LoggerInterface $logger, + private IJobList $jobList, + ) { + } + + /** + * 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, PostSetupJob::JOB_STATUS_CONFIG_KEY, PostSetupJob::JOB_STATUS_INIT); + + $adminUserId = $event->getAdminUsername(); + if ($adminUserId === null) { + $this->logger->warning('No admin user provided in InstallationCompletedEvent'); + return; + } + + $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..08c77e3 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,6 @@ + + + diff --git a/psalm.xml b/psalm.xml index e2853b7..6f28d5e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,21 +1,38 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 22f6d85..ba147cc 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,5 +10,14 @@ require_once __DIR__ . '/../../../tests/bootstrap.php'; require_once __DIR__ . '/../vendor/autoload.php'; +// Autoload test stubs for classes not available in the current OCP package +spl_autoload_register(function ($class) { + $stubsDir = __DIR__ . '/stubs/'; + $file = $stubsDir . str_replace('\\', '/', $class) . '.php'; + if (file_exists($file)) { + require_once $file; + } +}); + \OC_App::loadApp(OCA\NcwTools\AppInfo\Application::APP_ID); OC_Hook::clear(); diff --git a/tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php b/tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php new file mode 100644 index 0000000..576fddd --- /dev/null +++ b/tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php @@ -0,0 +1,99 @@ +getEMailAddress() === null) { + return null; + } + + $userId = $user->getUID(); + + if ($generatePasswordResetToken) { + $token = $this->secureRandom->generate( + 21, + ISecureRandom::CHAR_ALPHANUMERIC + ); + $tokenValue = $this->timeFactory->getTime() . ':' . $token; + $mailAddress = ($user->getEMailAddress() !== null) ? $user->getEMailAddress() : ''; + $encryptedValue = $this->crypto->encrypt($tokenValue, $mailAddress . $this->config->getSystemValue('secret')); + $this->config->setUserValue($user->getUID(), 'core', 'lostpassword', $encryptedValue); + } + + // Return a mock email template + return $this->mailer->createEMailTemplate('settings.Welcome'); + } + + /** + * Sends a welcome mail to $user + * + * @param IUser $user + * @param IEMailTemplate $emailTemplate + * @throws \Exception If mail could not be sent + */ + public function sendMail(IUser $user, IEMailTemplate $emailTemplate): void { + // Return early if user has no email (stub behavior) + if ($user->getEMailAddress() === null) { + return; + } + + // Simulate sending the mail + $message = $this->mailer->createMessage(); + $message->setTo([$user->getEMailAddress() => $user->getDisplayName()]); + $message->setFrom([$this->fromAddress => $this->themingDefaults->getName()]); + $message->useTemplate($emailTemplate); + $this->mailer->send($message); + } +} diff --git a/tests/stubs/OCP/Install/Events/InstallationCompletedEvent.php b/tests/stubs/OCP/Install/Events/InstallationCompletedEvent.php new file mode 100644 index 0000000..604cc03 --- /dev/null +++ b/tests/stubs/OCP/Install/Events/InstallationCompletedEvent.php @@ -0,0 +1,67 @@ +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/AppInfo/ApplicationTest.php b/tests/unit/AppInfo/ApplicationTest.php index ccde177..1207977 100644 --- a/tests/unit/AppInfo/ApplicationTest.php +++ b/tests/unit/AppInfo/ApplicationTest.php @@ -10,8 +10,10 @@ namespace OCA\NcwTools\Tests\Unit\AppInfo; use OCA\NcwTools\AppInfo\Application; +use OCA\NcwTools\Listeners\InstallationCompletedEventListener; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Install\Events\InstallationCompletedEvent; use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; @@ -37,9 +39,15 @@ public function testConstructorInitializesWithCorrectAppId(): void { } public function testRegisterMethodExists(): void { - // Test that register method can be called without errors - // Since the method is currently empty, we just verify it doesn't throw - $this->expectNotToPerformAssertions(); + // Test that register method registers the InstallationCompletedEvent listener + $this->registrationContext + ->expects($this->once()) + ->method('registerEventListener') + ->with( + InstallationCompletedEvent::class, + InstallationCompletedEventListener::class + ); + $this->application->register($this->registrationContext); } } diff --git a/tests/unit/BackgroundJob/PostSetupJobTest.php b/tests/unit/BackgroundJob/PostSetupJobTest.php new file mode 100644 index 0000000..dbb1ef6 --- /dev/null +++ b/tests/unit/BackgroundJob/PostSetupJobTest.php @@ -0,0 +1,318 @@ +logger = $this->createMock(LoggerInterface::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->config = $this->createMock(IConfig::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->clientService = $this->createMock(IClientService::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $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, + $this->config, + $this->userManager, + $this->clientService, + $this->timeFactory, + $this->jobList, + $this->welcomeMailHelper + ); + } + + public function testRunWithJobAlreadyDone(): void { + $this->appConfig->expects($this->once()) + ->method('getValueString') + ->with('ncw_tools', 'post_install', 'UNKNOWN') + ->willReturn('DONE'); + + $this->logger->expects($this->once()) + ->method('info') + ->with('Post-installation job already completed, removing from queue'); + + // Use reflection to call protected method + $reflection = new \ReflectionClass($this->job); + $method = $reflection->getMethod('run'); + $method->setAccessible(true); + $method->invoke($this->job, 'test-admin'); + } + + public function testRunWithUnknownStatus(): void { + $this->appConfig->expects($this->once()) + ->method('getValueString') + ->with('ncw_tools', 'post_install', 'UNKNOWN') + ->willReturn('UNKNOWN'); + + $this->logger->expects($this->once()) + ->method('warning') + ->with('Job status unknown, waiting for initialization'); + + // Use reflection to call protected method + $reflection = new \ReflectionClass($this->job); + $method = $reflection->getMethod('run'); + $method->setAccessible(true); + $method->invoke($this->job, 'test-admin'); + } + + public function testRunWithPendingStatus(): void { + $this->appConfig->expects($this->once()) + ->method('getValueString') + ->with('ncw_tools', 'post_install', 'UNKNOWN') + ->willReturn('PENDING'); + + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('overwrite.cli.url') + ->willReturn('https://example.com'); + + $client = $this->createMock(IClient::class); + $response = $this->createMock(IResponse::class); + $response->expects($this->atLeastOnce()) + ->method('getStatusCode') + ->willReturn(200); + + $client->expects($this->once()) + ->method('get') + ->with('https://example.com/status.php') + ->willReturn($response); + + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with('test-admin') + ->willReturn(false); + + // Expect info logs for job start/completion and system ready + $this->logger->expects($this->atLeastOnce()) + ->method('info'); + + $this->logger->expects($this->once()) + ->method('warning') + ->with('Admin user not found, cannot send welcome email', $this->anything()); + + // 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); + $method = $reflection->getMethod('run'); + $method->setAccessible(true); + $method->invoke($this->job, 'test-admin'); + } + + public function testRunWithUrlNotAvailable(): void { + $this->appConfig->expects($this->once()) + ->method('getValueString') + ->with('ncw_tools', 'post_install', 'UNKNOWN') + ->willReturn('PENDING'); + + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('overwrite.cli.url') + ->willReturn('https://example.com'); + + $client = $this->createMock(IClient::class); + $client->expects($this->once()) + ->method('get') + ->with('https://example.com/status.php') + ->willThrowException(new \Exception('Connection failed')); + + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + + // 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()) + ->method('setValueString'); + + // Use reflection to call protected method + $reflection = new \ReflectionClass($this->job); + $method = $reflection->getMethod('run'); + $method->setAccessible(true); + $method->invoke($this->job, 'test-admin'); + } + + public function testRunWithSuccessfulMailSend(): void { + $this->appConfig->expects($this->once()) + ->method('getValueString') + ->with('ncw_tools', 'post_install', 'UNKNOWN') + ->willReturn('PENDING'); + + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('overwrite.cli.url') + ->willReturn('https://example.com'); + + $client = $this->createMock(IClient::class); + $response = $this->createMock(IResponse::class); + $response->expects($this->atLeastOnce()) + ->method('getStatusCode') + ->willReturn(200); + + $client->expects($this->once()) + ->method('get') + ->with('https://example.com/status.php') + ->willReturn($response); + + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('test-admin'); + $user->method('getEMailAddress')->willReturn('admin@example.com'); + $user->method('getBackendClassName')->willReturn('Database'); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with('test-admin') + ->willReturn(true); + + $this->userManager->expects($this->once()) + ->method('get') + ->with('test-admin') + ->willReturn($user); + + // Expect debug logs (allow flexible count due to URL checking) + $this->logger->expects($this->atLeastOnce()) + ->method('debug'); + + // Expect welcome mail to be sent + $this->welcomeMailHelper->expects($this->once()) + ->method('sendWelcomeMail') + ->with($user, true); + + $this->appConfig->expects($this->once()) + ->method('setValueString') + ->with('ncw_tools', 'post_install', 'DONE'); + + // Use reflection to call protected method + $reflection = new \ReflectionClass($this->job); + $method = $reflection->getMethod('run'); + $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 new file mode 100644 index 0000000..ceae501 --- /dev/null +++ b/tests/unit/Helper/WelcomeMailHelperTest.php @@ -0,0 +1,163 @@ +defaults = $this->createMock(Defaults::class); + $this->crypto = $this->createMock(ICrypto::class); + $this->mailer = $this->createMock(IMailer::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->l10NFactory = $this->createMock(IFactory::class); + $this->secureRandom = $this->createMock(ISecureRandom::class); + $this->config = $this->createMock(IConfig::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + // Setup mocks required by NewUserMailHelper + $l10n = $this->createMock(IL10N::class); + $l10n->method('t')->willReturnArgument(0); + $this->l10NFactory->method('get')->willReturn($l10n); + $this->l10NFactory->method('getUserLanguage')->willReturn('en'); + + $emailTemplate = $this->createMock(IEMailTemplate::class); + $emailTemplate->method('addHeader')->willReturnSelf(); + $emailTemplate->method('addHeading')->willReturnSelf(); + $emailTemplate->method('addBodyText')->willReturnSelf(); + $emailTemplate->method('addBodyButton')->willReturnSelf(); + $emailTemplate->method('addBodyButtonGroup')->willReturnSelf(); + $emailTemplate->method('addFooter')->willReturnSelf(); + $emailTemplate->method('setSubject')->willReturnSelf(); + $this->mailer->method('createEMailTemplate')->willReturn($emailTemplate); + + $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); + + $this->defaults->method('getName')->willReturn('Nextcloud'); + $this->urlGenerator->method('getAbsoluteURL')->willReturn('https://example.com'); + $this->urlGenerator->method('linkToRouteAbsolute')->willReturn('https://example.com/reset'); + $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, + $this->crypto, + $this->mailer, + $this->urlGenerator, + $this->l10NFactory, + $this->secureRandom, + $this->config, + $this->timeFactory + ); + } + + public function testSendWelcomeMailWithPasswordResetToken(): void { + $user = $this->createMock(IUser::class); + $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->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 { + $helper = new WelcomeMailHelper( + $this->defaults, + $this->crypto, + $this->mailer, + $this->urlGenerator, + $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 new file mode 100644 index 0000000..84bca29 --- /dev/null +++ b/tests/unit/Listeners/InstallationCompletedEventListenerTest.php @@ -0,0 +1,99 @@ +appConfig = $this->createMock(IAppConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->jobList = $this->createMock(IJobList::class); + + $this->listener = new InstallationCompletedEventListener( + $this->appConfig, + $this->logger, + $this->jobList + ); + } + + public function testHandleSetsAppConfigAndAddsJob(): void { + $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') + ->with(PostSetupJob::class, 'admin'); + + // Expect debug logs + $this->logger->expects($this->atLeastOnce()) + ->method('debug'); + + $this->listener->handle($event); + } + + 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 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); + } +}