From 0ead7797c419cdaed583e5822dfd8455a8601c7e Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Wed, 21 Jan 2026 18:44:47 +0100 Subject: [PATCH 1/2] chore: add .editorconfig for consistent coding style Add EditorConfig file to maintain consistent coding styles across different editors and IDEs. Configures indent styles for PHP (tabs), JSON/YAML (spaces), and other file types. Signed-off-by: Misha M.-Kupriyanov --- .editorconfig | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .editorconfig 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 From 444034b16457ea161c4e6583eee839a704f5892b Mon Sep 17 00:00:00 2001 From: Arsalan Ul Haq Date: Mon, 19 Jan 2026 15:01:10 +0100 Subject: [PATCH 2/2] feat(installation): implement post setup job and welcome email functionality Add a new PostSetupJob that sends a welcome email to the admin user after installation completion. This job checks the installation status and ensures the SMTP configuration is set correctly. Additionally, an InstallationCompletedEventListener is introduced to trigger the PostSetupJob upon installation completion. This enhancement improves user onboarding by automating the welcome email process. Signed-off-by: Arsalan Ul Haq Sohni --- README.md | 16 + composer.json | 5 + lib/AppInfo/Application.php | 7 +- lib/BackgroundJob/PostSetupJob.php | 162 +++++++++ lib/Helper/WelcomeMailHelper.php | 67 ++++ .../InstallationCompletedEventListener.php | 58 ++++ psalm-baseline.xml | 6 + psalm.xml | 43 ++- tests/bootstrap.php | 9 + .../OCA/Settings/Mailer/NewUserMailHelper.php | 99 ++++++ .../Events/InstallationCompletedEvent.php | 67 ++++ tests/unit/AppInfo/ApplicationTest.php | 14 +- tests/unit/BackgroundJob/PostSetupJobTest.php | 318 ++++++++++++++++++ tests/unit/Helper/WelcomeMailHelperTest.php | 163 +++++++++ ...InstallationCompletedEventListenerTest.php | 99 ++++++ 15 files changed, 1113 insertions(+), 20 deletions(-) create mode 100644 lib/BackgroundJob/PostSetupJob.php create mode 100644 lib/Helper/WelcomeMailHelper.php create mode 100644 lib/Listeners/InstallationCompletedEventListener.php create mode 100644 psalm-baseline.xml create mode 100644 tests/stubs/OCA/Settings/Mailer/NewUserMailHelper.php create mode 100644 tests/stubs/OCP/Install/Events/InstallationCompletedEvent.php create mode 100644 tests/unit/BackgroundJob/PostSetupJobTest.php create mode 100644 tests/unit/Helper/WelcomeMailHelperTest.php create mode 100644 tests/unit/Listeners/InstallationCompletedEventListenerTest.php 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); + } +}