From be29ea57b39e40390dc22a3d3acbc18da312e694 Mon Sep 17 00:00:00 2001 From: Peter Ringelmann Date: Mon, 3 Aug 2026 09:10:03 +0200 Subject: [PATCH] fix(provisioning_api): read newUser.sendEmail as a boolean Signed-off-by: Peter Ringelmann --- .../lib/Controller/UsersController.php | 4 +- .../tests/Controller/UsersControllerTest.php | 69 +++++++++++++++++++ build/psalm-baseline.xml | 1 - core/Command/User/Add.php | 2 +- tests/Core/Command/User/AddTest.php | 4 +- tests/lib/AppConfigIntegrationTest.php | 28 ++++++++ 6 files changed, 103 insertions(+), 5 deletions(-) diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index d8cd1a4e13006..1b76d1bc61e46 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -37,6 +37,7 @@ use OCP\Files\IRootFolder; use OCP\Group\ISubAdmin; use OCP\HintException; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IGroup; use OCP\IGroupManager; @@ -83,6 +84,7 @@ public function __construct( private IEventDispatcher $eventDispatcher, private IPhoneNumberUtil $phoneNumberUtil, private IAppManager $appManager, + private IAppConfig $appConfig, GroupDisplayNameCache $groupDisplayNameCache, ) { parent::__construct( @@ -596,7 +598,7 @@ public function addUser( // Send new user mail only if a mail is set if ($email !== '') { $newUser->setSystemEMailAddress($email); - if ($this->config->getAppValue('core', 'newUser.sendEmail', 'yes') === 'yes') { + if ($this->appConfig->getValueBool('core', 'newUser.sendEmail', true)) { try { $emailTemplate = $this->newUserMailHelper->generateTemplate($newUser, $generatePasswordResetToken); $this->newUserMailHelper->sendMail($newUser, $emailTemplate); diff --git a/apps/provisioning_api/tests/Controller/UsersControllerTest.php b/apps/provisioning_api/tests/Controller/UsersControllerTest.php index 492b6666f4ba5..84e4f15b4b913 100644 --- a/apps/provisioning_api/tests/Controller/UsersControllerTest.php +++ b/apps/provisioning_api/tests/Controller/UsersControllerTest.php @@ -27,6 +27,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\IRootFolder; use OCP\Group\ISubAdmin; +use OCP\IAppConfig; use OCP\IConfig; use OCP\IGroup; use OCP\IL10N; @@ -67,6 +68,7 @@ class UsersControllerTest extends TestCase { private IRootFolder $rootFolder; private IPhoneNumberUtil $phoneNumberUtil; private IAppManager $appManager; + private IAppConfig&MockObject $appConfig; private GroupDisplayNameCache&MockObject $groupDisplayNameCache; protected function setUp(): void { @@ -89,6 +91,7 @@ protected function setUp(): void { $this->eventDispatcher = $this->createMock(IEventDispatcher::class); $this->phoneNumberUtil = new PhoneNumberUtil(); $this->appManager = $this->createMock(IAppManager::class); + $this->appConfig = $this->createMock(IAppConfig::class); $this->rootFolder = $this->createMock(IRootFolder::class); $this->groupDisplayNameCache = $this->createMock(GroupDisplayNameCache::class); @@ -117,6 +120,7 @@ protected function setUp(): void { $this->eventDispatcher, $this->phoneNumberUtil, $this->appManager, + $this->appConfig, $this->groupDisplayNameCache, ]) ->onlyMethods(['fillStorageInfo']) @@ -270,6 +274,7 @@ public function testGetUsersDetailsReturnsEmptyGroupsList(): void { $this->eventDispatcher, $this->phoneNumberUtil, $this->appManager, + $this->appConfig, $this->groupDisplayNameCache, ]) ->onlyMethods(['getUserData']) @@ -587,6 +592,7 @@ public function testAddUserSuccessfulWithDisplayName(): void { $this->eventDispatcher, $this->phoneNumberUtil, $this->appManager, + $this->appConfig, $this->groupDisplayNameCache, ]) ->onlyMethods(['editUser']) @@ -730,6 +736,67 @@ public function testAddUserSuccessfulGeneratePassword(): void { )); } + /** + * `newUser.sendEmail` has to be read as a boolean. It is stored as an untyped + * 'yes'/'no' string on instances created before Nextcloud 33 and as a typed + * boolean once the account settings toggle has been used, so comparing it to + * the string 'yes' silently skipped the mail on upgraded instances. + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataAddUserWelcomeMail')] + public function testAddUserSendsWelcomeMailWhenEnabled(bool $enabled): void { + $this->appConfig + ->expects($this->atLeastOnce()) + ->method('getValueBool') + ->with('core', 'newUser.sendEmail', true) + ->willReturn($enabled); + + $newUser = $this->createMock(IUser::class); + $newUser->expects($this->once()) + ->method('setSystemEMailAddress') + ->with('foo@bar.com'); + $this->userManager + ->expects($this->once()) + ->method('userExists') + ->with('NewUser') + ->willReturn(false); + $this->userManager + ->expects($this->once()) + ->method('createUser') + ->willReturn($newUser); + $loggedInUser = $this->createMock(IUser::class); + $loggedInUser + ->method('getUID') + ->willReturn('adminUser'); + $this->userSession + ->expects($this->once()) + ->method('getUser') + ->willReturn($loggedInUser); + $this->groupManager + ->expects($this->once()) + ->method('isAdmin') + ->with('adminUser') + ->willReturn(true); + + $emailTemplate = $this->createMock(IEMailTemplate::class); + $this->newUserMailHelper + ->expects($enabled ? $this->once() : $this->never()) + ->method('generateTemplate') + ->willReturn($emailTemplate); + $this->newUserMailHelper + ->expects($enabled ? $this->once() : $this->never()) + ->method('sendMail') + ->with($newUser, $emailTemplate); + + $this->api->addUser('NewUser', 'PasswordOfTheNewUser', '', 'foo@bar.com'); + } + + public static function dataAddUserWelcomeMail(): array { + return [ + 'enabled' => [true], + 'disabled' => [false], + ]; + } + public function testAddUserSuccessfulLowercaseEmail(): void { $this->userManager ->expects($this->once()) @@ -3928,6 +3995,7 @@ public function testGetCurrentUserLoggedIn(): void { $this->eventDispatcher, $this->phoneNumberUtil, $this->appManager, + $this->appConfig, $this->groupDisplayNameCache, ]) ->onlyMethods(['getUserData']) @@ -4023,6 +4091,7 @@ public function testGetUser(): void { $this->eventDispatcher, $this->phoneNumberUtil, $this->appManager, + $this->appConfig, $this->groupDisplayNameCache, ]) ->onlyMethods(['getUserData']) diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 530548f1896e6..4663e682ff9ad 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2204,7 +2204,6 @@ - diff --git a/core/Command/User/Add.php b/core/Command/User/Add.php index 440bced4c2e37..4870c192dbf9b 100644 --- a/core/Command/User/Add.php +++ b/core/Command/User/Add.php @@ -180,7 +180,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $user->setSystemEMailAddress($email); - if ($this->appConfig->getValueString('core', 'newUser.sendEmail', 'yes') === 'yes') { + if ($this->appConfig->getValueBool('core', 'newUser.sendEmail', true)) { try { $this->mailHelper->sendMail($user, $this->mailHelper->generateTemplate($user, true)); $output->writeln('Welcome email sent to ' . $email); diff --git a/tests/Core/Command/User/AddTest.php b/tests/Core/Command/User/AddTest.php index 748a9b60ef632..9adf30ac49f9e 100644 --- a/tests/Core/Command/User/AddTest.php +++ b/tests/Core/Command/User/AddTest.php @@ -99,8 +99,8 @@ public function testAddEmail( $this->userManager->method('createUser') ->willReturn($this->user); - $this->appConfig->method('getValueString') - ->willReturn($shouldSendEmail ? 'yes' : 'no'); + $this->appConfig->method('getValueBool') + ->willReturn($shouldSendEmail); $this->mailHelper->method('generateTemplate') ->willReturn(static::createMock(IEMailTemplate::class)); diff --git a/tests/lib/AppConfigIntegrationTest.php b/tests/lib/AppConfigIntegrationTest.php index b0e400dd9e3fc..2bd0eea90be6f 100644 --- a/tests/lib/AppConfigIntegrationTest.php +++ b/tests/lib/AppConfigIntegrationTest.php @@ -519,6 +519,34 @@ public function testGetValueBool(): void { $this->assertSame(true, $config->getValueBool('typed', 'bool')); } + /** + * Untyped values predate the typed config API and are still in the database of + * every upgraded instance, so they have to keep resolving to a boolean. The + * deprecated setter is used on purpose, as it is the only way to write a value + * without a type. + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataUntypedBool')] + public function testGetValueBoolOnUntypedValue(string $stored, bool $expected): void { + /** @var AppConfig $config */ + $config = $this->generateAppConfig(); + $config->setValue('feed', 'untyped-bool', $stored); + + $this->assertSame($expected, $config->getValueBool('feed', 'untyped-bool')); + } + + public static function dataUntypedBool(): array { + return [ + 'yes' => ['yes', true], + 'no' => ['no', false], + 'true' => ['true', true], + 'false' => ['false', false], + 'on' => ['on', true], + '1' => ['1', true], + '0' => ['0', false], + 'empty' => ['', false], + ]; + } + public function testGetValueBoolOnUnknownAppReturnsDefault(): void { $config = $this->generateAppConfig(); $this->assertSame(false, $config->getValueBool('typed-1', 'bool', false));