aboutsummaryrefslogtreecommitdiffstats
path: root/tests/Core
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Core')
-rw-r--r--tests/Core/Command/Apps/AppsDisableTest.php71
-rw-r--r--tests/Core/Command/Apps/AppsEnableTest.php84
-rw-r--r--tests/Core/Command/Config/App/DeleteConfigTest.php99
-rw-r--r--tests/Core/Command/Config/App/GetConfigTest.php138
-rw-r--r--tests/Core/Command/Config/App/SetConfigTest.php107
-rw-r--r--tests/Core/Command/Config/ImportTest.php169
-rw-r--r--tests/Core/Command/Config/ListConfigsTest.php328
-rw-r--r--tests/Core/Command/Config/System/CastHelperTest.php66
-rw-r--r--tests/Core/Command/Config/System/DeleteConfigTest.php207
-rw-r--r--tests/Core/Command/Config/System/GetConfigTest.php161
-rw-r--r--tests/Core/Command/Config/System/SetConfigTest.php115
-rw-r--r--tests/Core/Command/Encryption/ChangeKeyStorageRootTest.php360
-rw-r--r--tests/Core/Command/Encryption/DecryptAllTest.php216
-rw-r--r--tests/Core/Command/Encryption/DisableTest.php74
-rw-r--r--tests/Core/Command/Encryption/EnableTest.php101
-rw-r--r--tests/Core/Command/Encryption/EncryptAllTest.php94
-rw-r--r--tests/Core/Command/Encryption/SetDefaultModuleTest.php130
-rw-r--r--tests/Core/Command/Group/AddTest.php80
-rw-r--r--tests/Core/Command/Group/AddUserTest.php101
-rw-r--r--tests/Core/Command/Group/DeleteTest.php132
-rw-r--r--tests/Core/Command/Group/InfoTest.php98
-rw-r--r--tests/Core/Command/Group/ListCommandTest.php204
-rw-r--r--tests/Core/Command/Group/RemoveUserTest.php101
-rw-r--r--tests/Core/Command/Log/FileTest.php112
-rw-r--r--tests/Core/Command/Log/ManageTest.php164
-rw-r--r--tests/Core/Command/Maintenance/DataFingerprintTest.php53
-rw-r--r--tests/Core/Command/Maintenance/Mimetype/UpdateDBTest.php174
-rw-r--r--tests/Core/Command/Maintenance/ModeTest.php151
-rw-r--r--tests/Core/Command/Maintenance/UpdateTheme.php63
-rw-r--r--tests/Core/Command/Preview/CleanupTest.php175
-rw-r--r--tests/Core/Command/Preview/RepairTest.php153
-rw-r--r--tests/Core/Command/SystemTag/AddTest.php121
-rw-r--r--tests/Core/Command/SystemTag/DeleteTest.php83
-rw-r--r--tests/Core/Command/SystemTag/EditTest.php186
-rw-r--r--tests/Core/Command/SystemTag/ListCommandTest.php96
-rw-r--r--tests/Core/Command/TwoFactorAuth/CleanupTest.php52
-rw-r--r--tests/Core/Command/TwoFactorAuth/DisableTest.php94
-rw-r--r--tests/Core/Command/TwoFactorAuth/EnableTest.php94
-rw-r--r--tests/Core/Command/TwoFactorAuth/EnforceTest.php128
-rw-r--r--tests/Core/Command/TwoFactorAuth/StateTest.php94
-rw-r--r--tests/Core/Command/User/AddTest.php152
-rw-r--r--tests/Core/Command/User/AuthTokens/DeleteTest.php166
-rw-r--r--tests/Core/Command/User/DeleteTest.php96
-rw-r--r--tests/Core/Command/User/DisableTest.php78
-rw-r--r--tests/Core/Command/User/EnableTest.php78
-rw-r--r--tests/Core/Command/User/LastSeenTest.php95
-rw-r--r--tests/Core/Command/User/ProfileTest.php465
-rw-r--r--tests/Core/Command/User/SettingTest.php452
-rw-r--r--tests/Core/Controller/AppPasswordControllerTest.php249
-rw-r--r--tests/Core/Controller/AutoCompleteControllerTest.php173
-rw-r--r--tests/Core/Controller/AvatarControllerTest.php567
-rw-r--r--tests/Core/Controller/CSRFTokenControllerTest.php62
-rw-r--r--tests/Core/Controller/ChangePasswordControllerTest.php193
-rw-r--r--tests/Core/Controller/ClientFlowLoginControllerTest.php685
-rw-r--r--tests/Core/Controller/ClientFlowLoginV2ControllerTest.php394
-rw-r--r--tests/Core/Controller/ContactsMenuControllerTest.php88
-rw-r--r--tests/Core/Controller/CssControllerTest.php171
-rw-r--r--tests/Core/Controller/GuestAvatarControllerTest.php95
-rw-r--r--tests/Core/Controller/JsControllerTest.php171
-rw-r--r--tests/Core/Controller/LoginControllerTest.php696
-rw-r--r--tests/Core/Controller/LostControllerTest.php756
-rw-r--r--tests/Core/Controller/NavigationControllerTest.php138
-rw-r--r--tests/Core/Controller/OCSControllerTest.php218
-rw-r--r--tests/Core/Controller/PreviewControllerTest.php367
-rw-r--r--tests/Core/Controller/TwoFactorChallengeControllerTest.php446
-rw-r--r--tests/Core/Controller/UserControllerTest.php60
-rw-r--r--tests/Core/Controller/WellKnownControllerTest.php69
-rw-r--r--tests/Core/Controller/WipeControllerTest.php101
-rw-r--r--tests/Core/Data/LoginFlowV2CredentialsTest.php47
-rw-r--r--tests/Core/Middleware/TwoFactorMiddlewareTest.php286
-rw-r--r--tests/Core/Service/LoginFlowV2ServiceUnitTest.php461
71 files changed, 13304 insertions, 0 deletions
diff --git a/tests/Core/Command/Apps/AppsDisableTest.php b/tests/Core/Command/Apps/AppsDisableTest.php
new file mode 100644
index 00000000000..117af958054
--- /dev/null
+++ b/tests/Core/Command/Apps/AppsDisableTest.php
@@ -0,0 +1,71 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Command\Config;
+
+use OC\Core\Command\App\Disable;
+use OCP\App\IAppManager;
+use OCP\Server;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+/**
+ * Class AppsDisableTest
+ *
+ * @group DB
+ */
+class AppsDisableTest extends TestCase {
+ /** @var CommandTester */
+ private $commandTester;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $command = new Disable(
+ Server::get(IAppManager::class)
+ );
+
+ $this->commandTester = new CommandTester($command);
+
+ Server::get(IAppManager::class)->enableApp('admin_audit');
+ Server::get(IAppManager::class)->enableApp('comments');
+ }
+
+ /**
+ * @param $appId
+ * @param $groups
+ * @param $statusCode
+ * @param $pattern
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCommandInput')]
+ public function testCommandInput($appId, $statusCode, $pattern): void {
+ $input = ['app-id' => $appId];
+
+ $this->commandTester->execute($input);
+
+ $this->assertMatchesRegularExpression('/' . $pattern . '/', $this->commandTester->getDisplay());
+ $this->assertSame($statusCode, $this->commandTester->getStatusCode());
+ }
+
+ public static function dataCommandInput(): array {
+ return [
+ [['admin_audit'], 0, 'admin_audit ([\d\.]*) disabled'],
+ [['comments'], 0, 'comments ([\d\.]*) disabled'],
+ [['invalid_app'], 0, 'No such app enabled: invalid_app'],
+
+ [['admin_audit', 'comments'], 0, "admin_audit ([\d\.]*) disabled\ncomments ([\d\.]*) disabled"],
+ [['admin_audit', 'comments', 'invalid_app'], 0, "admin_audit ([\d\.]*) disabled\ncomments ([\d\.]*) disabled\nNo such app enabled: invalid_app"],
+
+ [['files'], 2, "files can't be disabled"],
+ [['provisioning_api'], 2, "provisioning_api can't be disabled"],
+
+ [['files', 'admin_audit'], 2, "files can't be disabled.\nadmin_audit ([\d\.]*) disabled"],
+ [['provisioning_api', 'comments'], 2, "provisioning_api can't be disabled.\ncomments ([\d\.]*) disabled"],
+ ];
+ }
+}
diff --git a/tests/Core/Command/Apps/AppsEnableTest.php b/tests/Core/Command/Apps/AppsEnableTest.php
new file mode 100644
index 00000000000..604c670ae15
--- /dev/null
+++ b/tests/Core/Command/Apps/AppsEnableTest.php
@@ -0,0 +1,84 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Command\Config;
+
+use OC\Core\Command\App\Enable;
+use OC\Installer;
+use OCP\App\IAppManager;
+use OCP\IGroupManager;
+use OCP\Server;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+/**
+ * Class AppsEnableTest
+ *
+ * @group DB
+ */
+class AppsEnableTest extends TestCase {
+ /** @var CommandTester */
+ private $commandTester;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $command = new Enable(
+ Server::get(IAppManager::class),
+ Server::get(IGroupManager::class),
+ Server::get(Installer::class),
+ );
+
+ $this->commandTester = new CommandTester($command);
+
+ Server::get(IAppManager::class)->disableApp('admin_audit');
+ Server::get(IAppManager::class)->disableApp('comments');
+ }
+
+ /**
+ * @param $appId
+ * @param $groups
+ * @param $statusCode
+ * @param $pattern
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCommandInput')]
+ public function testCommandInput($appId, $groups, $statusCode, $pattern): void {
+ $input = ['app-id' => $appId];
+
+ if (is_array($groups)) {
+ $input['--groups'] = $groups;
+ }
+
+ $this->commandTester->execute($input);
+
+ $this->assertMatchesRegularExpression('/' . $pattern . '/', $this->commandTester->getDisplay());
+ $this->assertSame($statusCode, $this->commandTester->getStatusCode());
+ }
+
+ public static function dataCommandInput(): array {
+ return [
+ [['admin_audit'], null, 0, 'admin_audit ([\d\.]*) enabled'],
+ [['comments'], null, 0, 'comments ([\d\.]*) enabled'],
+ [['comments', 'comments'], null, 0, "comments ([\d\.]*) enabled\ncomments already enabled"],
+ [['invalid_app'], null, 1, 'Could not download app invalid_app'],
+
+ [['admin_audit', 'comments'], null, 0, "admin_audit ([\d\.]*) enabled\ncomments ([\d\.]*) enabled"],
+ [['admin_audit', 'comments', 'invalid_app'], null, 1, "admin_audit ([\d\.]*) enabled\ncomments ([\d\.]*) enabled\nCould not download app invalid_app"],
+
+ [['admin_audit'], ['admin'], 1, "admin_audit can't be enabled for groups"],
+ [['comments'], ['admin'], 1, "comments can't be enabled for groups"],
+
+ [['updatenotification'], ['admin'], 0, 'updatenotification ([\d\.]*) enabled for groups: admin'],
+ [['updatenotification', 'dashboard'], ['admin'], 0, "updatenotification ([\d\.]*) enabled for groups: admin\ndashboard ([\d\.]*) enabled for groups: admin"],
+
+ [['updatenotification'], ['admin', 'invalid_group'], 0, 'updatenotification ([\d\.]*) enabled for groups: admin'],
+ [['updatenotification', 'dashboard'], ['admin', 'invalid_group'], 0, "updatenotification ([\d\.]*) enabled for groups: admin\ndashboard ([\d\.]*) enabled for groups: admin"],
+ [['updatenotification', 'dashboard', 'invalid_app'], ['admin', 'invalid_group'], 1, "updatenotification ([\d\.]*) enabled for groups: admin\ndashboard ([\d\.]*) enabled for groups: admin\nCould not download app invalid_app"],
+ ];
+ }
+}
diff --git a/tests/Core/Command/Config/App/DeleteConfigTest.php b/tests/Core/Command/Config/App/DeleteConfigTest.php
new file mode 100644
index 00000000000..9e43f389214
--- /dev/null
+++ b/tests/Core/Command/Config/App/DeleteConfigTest.php
@@ -0,0 +1,99 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Config\App;
+
+use OC\Config\ConfigManager;
+use OC\Core\Command\Config\App\DeleteConfig;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DeleteConfigTest extends TestCase {
+ protected IAppConfig&MockObject $appConfig;
+ protected ConfigManager&MockObject $configManager;
+ protected InputInterface&MockObject $consoleInput;
+ protected OutputInterface&MockObject $consoleOutput;
+ protected Command $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appConfig = $this->createMock(IAppConfig::class);
+ $this->configManager = $this->createMock(ConfigManager::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+
+ $this->command = new DeleteConfig($this->appConfig, $this->configManager);
+ }
+
+
+ public static function dataDelete(): array {
+ return [
+ [
+ 'name',
+ true,
+ true,
+ 0,
+ 'info',
+ ],
+ [
+ 'name',
+ true,
+ false,
+ 0,
+ 'info',
+ ],
+ [
+ 'name',
+ false,
+ false,
+ 0,
+ 'info',
+ ],
+ [
+ 'name',
+ false,
+ true,
+ 1,
+ 'error',
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataDelete')]
+ public function testDelete(string $configName, bool $configExists, bool $checkIfExists, int $expectedReturn, string $expectedMessage): void {
+ $this->appConfig->expects(($checkIfExists) ? $this->once() : $this->never())
+ ->method('getKeys')
+ ->with('app-name')
+ ->willReturn($configExists ? [$configName] : []);
+
+ $this->appConfig->expects(($expectedReturn === 0) ? $this->once() : $this->never())
+ ->method('deleteKey')
+ ->with('app-name', $configName);
+
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['app', 'app-name'],
+ ['name', $configName],
+ ]);
+ $this->consoleInput->method('hasParameterOption')
+ ->with('--error-if-not-exists')
+ ->willReturn($checkIfExists);
+
+ $this->consoleOutput->method('writeln')
+ ->with($this->stringContains($expectedMessage));
+
+ $this->assertSame($expectedReturn, self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+}
diff --git a/tests/Core/Command/Config/App/GetConfigTest.php b/tests/Core/Command/Config/App/GetConfigTest.php
new file mode 100644
index 00000000000..13392cddf55
--- /dev/null
+++ b/tests/Core/Command/Config/App/GetConfigTest.php
@@ -0,0 +1,138 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Config\App;
+
+use OC\Config\ConfigManager;
+use OC\Core\Command\Config\App\GetConfig;
+use OCP\Exceptions\AppConfigUnknownKeyException;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class GetConfigTest extends TestCase {
+ protected IAppConfig&MockObject $appConfig;
+ protected ConfigManager&MockObject $configManager;
+ protected InputInterface&MockObject $consoleInput;
+ protected OutputInterface&MockObject $consoleOutput;
+ protected Command $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appConfig = $this->createMock(IAppConfig::class);
+ $this->configManager = $this->createMock(ConfigManager::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+
+ $this->command = new GetConfig($this->appConfig, $this->configManager);
+ }
+
+
+ public static function dataGet(): array {
+ return [
+ // String output as json
+ ['name', 'newvalue', true, null, false, 'json', 0, json_encode('newvalue')],
+ // String output as plain text
+ ['name', 'newvalue', true, null, false, 'plain', 0, 'newvalue'],
+ // String falling back to default output as json
+ ['name', null, false, 'newvalue', true, 'json', 0, json_encode('newvalue')],
+ // String falling back without default: error
+ ['name', null, false, null, false, 'json', 1, null],
+
+ // Int "0" output as json/plain
+ ['name', 0, true, null, false, 'json', 0, json_encode(0)],
+ ['name', 0, true, null, false, 'plain', 0, '0'],
+ // Int "1" output as json/plain
+ ['name', 1, true, null, false, 'json', 0, json_encode(1)],
+ ['name', 1, true, null, false, 'plain', 0, '1'],
+
+ // Bool "true" output as json/plain
+ ['name', true, true, null, false, 'json', 0, json_encode(true)],
+ ['name', true, true, null, false, 'plain', 0, 'true'],
+ // Bool "false" output as json/plain
+ ['name', false, true, null, false, 'json', 0, json_encode(false)],
+ ['name', false, true, null, false, 'plain', 0, 'false'],
+
+ // Null output as json/plain
+ ['name', null, true, null, false, 'json', 0, json_encode(null)],
+ ['name', null, true, null, false, 'plain', 0, 'null'],
+
+ // Array output as json/plain
+ ['name', ['a', 'b'], true, null, false, 'json', 0, json_encode(['a', 'b'])],
+ ['name', ['a', 'b'], true, null, false, 'plain', 0, "a\nb"],
+ // Key array output as json/plain
+ ['name', [0 => 'a', 1 => 'b'], true, null, false, 'json', 0, json_encode(['a', 'b'])],
+ ['name', [0 => 'a', 1 => 'b'], true, null, false, 'plain', 0, "a\nb"],
+ // Associative array output as json/plain
+ ['name', ['a' => 1, 'b' => 2], true, null, false, 'json', 0, json_encode(['a' => 1, 'b' => 2])],
+ ['name', ['a' => 1, 'b' => 2], true, null, false, 'plain', 0, "a: 1\nb: 2"],
+
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGet')]
+ public function testGet(string $configName, mixed $value, bool $configExists, mixed $defaultValue, bool $hasDefault, string $outputFormat, int $expectedReturn, ?string $expectedMessage): void {
+ if (!$expectedReturn) {
+ if ($configExists) {
+ $this->appConfig->expects($this->once())
+ ->method('getDetails')
+ ->with('app-name', $configName)
+ ->willReturn(['value' => $value]);
+ }
+ }
+
+ if (!$configExists) {
+ $this->appConfig->expects($this->once())
+ ->method('getDetails')
+ ->with('app-name', $configName)
+ ->willThrowException(new AppConfigUnknownKeyException());
+ }
+
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['app', 'app-name'],
+ ['name', $configName],
+ ]);
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['default-value', $defaultValue],
+ ['output', $outputFormat],
+ ]);
+ $this->consoleInput->method('hasParameterOption')
+ ->willReturnMap([
+ ['--output', false, true],
+ ['--default-value', false, $hasDefault],
+ ]);
+
+ if ($expectedMessage !== null) {
+ global $output;
+
+ $output = '';
+ $this->consoleOutput->method('writeln')
+ ->willReturnCallback(function ($value) {
+ global $output;
+ $output .= $value . "\n";
+ return $output;
+ });
+ }
+
+ $this->assertSame($expectedReturn, self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+
+ if ($expectedMessage !== null) {
+ global $output;
+ // Remove the trailing newline
+ $this->assertSame($expectedMessage, substr($output, 0, -1));
+ }
+ }
+}
diff --git a/tests/Core/Command/Config/App/SetConfigTest.php b/tests/Core/Command/Config/App/SetConfigTest.php
new file mode 100644
index 00000000000..a5c62368163
--- /dev/null
+++ b/tests/Core/Command/Config/App/SetConfigTest.php
@@ -0,0 +1,107 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Config\App;
+
+use OC\AppConfig;
+use OC\Config\ConfigManager;
+use OC\Core\Command\Config\App\SetConfig;
+use OCP\Exceptions\AppConfigUnknownKeyException;
+use OCP\IAppConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class SetConfigTest extends TestCase {
+ protected IAppConfig&MockObject $appConfig;
+ protected ConfigManager&MockObject $configManager;
+ protected InputInterface&MockObject $consoleInput;
+ protected OutputInterface&MockObject $consoleOutput;
+ protected Command $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->appConfig = $this->createMock(AppConfig::class);
+ $this->configManager = $this->createMock(ConfigManager::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+
+ $this->command = new SetConfig($this->appConfig, $this->configManager);
+ }
+
+
+ public static function dataSet(): array {
+ return [
+ [
+ 'name',
+ 'newvalue',
+ true,
+ true,
+ true,
+ 'info',
+ ],
+ [
+ 'name',
+ 'newvalue',
+ false,
+ true,
+ false,
+ 'comment',
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSet')]
+ public function testSet(string $configName, mixed $newValue, bool $configExists, bool $updateOnly, bool $updated, string $expectedMessage): void {
+ $this->appConfig->method('hasKey')
+ ->with('app-name', $configName)
+ ->willReturn($configExists);
+
+ if (!$configExists) {
+ $this->appConfig->method('getValueType')
+ ->willThrowException(new AppConfigUnknownKeyException());
+ } else {
+ $this->appConfig->method('getValueType')
+ ->willReturn(IAppConfig::VALUE_MIXED);
+ }
+
+ if ($updated) {
+ $this->appConfig->expects($this->once())
+ ->method('setValueMixed')
+ ->with('app-name', $configName, $newValue);
+ }
+
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['app', 'app-name'],
+ ['name', $configName],
+ ]);
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['value', $newValue],
+ ['lazy', null],
+ ['sensitive', null],
+ ['no-interaction', true],
+ ]);
+ $this->consoleInput->method('hasParameterOption')
+ ->willReturnMap([
+ ['--type', false, false],
+ ['--value', false, true],
+ ['--update-only', false, $updateOnly]
+ ]);
+ $this->consoleOutput->method('writeln')
+ ->with($this->stringContains($expectedMessage));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Config/ImportTest.php b/tests/Core/Command/Config/ImportTest.php
new file mode 100644
index 00000000000..14cdd714d12
--- /dev/null
+++ b/tests/Core/Command/Config/ImportTest.php
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Config;
+
+use OC\Core\Command\Config\Import;
+use OCP\IConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class ImportTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $config;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $config = $this->config = $this->getMockBuilder(IConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var IConfig $config */
+ $this->command = new Import($config);
+ }
+
+ public static function validateAppsArrayData(): array {
+ return [
+ [0],
+ [1],
+ [null],
+ ['new \Exception()'],
+ [json_encode([])],
+ ];
+ }
+
+ /**
+ * @param mixed $configValue
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('validateAppsArrayData')]
+ public function testValidateAppsArray($configValue): void {
+ $this->invokePrivate($this->command, 'validateAppsArray', [['app' => ['name' => $configValue]]]);
+ $this->assertTrue(true, 'Asserting that no exception is thrown');
+ }
+
+ public static function validateAppsArrayThrowsData(): array {
+ return [
+ [false],
+ [true],
+ [[]],
+ [new \Exception()],
+ ];
+ }
+
+ /**
+ * @param mixed $configValue
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('validateAppsArrayThrowsData')]
+ public function testValidateAppsArrayThrows($configValue): void {
+ try {
+ $this->invokePrivate($this->command, 'validateAppsArray', [['app' => ['name' => $configValue]]]);
+ $this->fail('Did not throw expected UnexpectedValueException');
+ } catch (\UnexpectedValueException $e) {
+ $this->assertStringStartsWith('Invalid app config value for "app":"name".', $e->getMessage());
+ }
+ }
+
+ public static function checkTypeRecursivelyData(): array {
+ return [
+ [0],
+ [1],
+ [null],
+ ['new \Exception()'],
+ [json_encode([])],
+ [false],
+ [true],
+ [[]],
+ [['string']],
+ [['test' => 'string']],
+ [['test' => ['sub' => 'string']]],
+ ];
+ }
+
+ /**
+ * @param mixed $configValue
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('checkTypeRecursivelyData')]
+ public function testCheckTypeRecursively($configValue): void {
+ $this->invokePrivate($this->command, 'checkTypeRecursively', [$configValue, 'name']);
+ $this->assertTrue(true, 'Asserting that no exception is thrown');
+ }
+
+ public static function checkTypeRecursivelyThrowsData(): array {
+ return [
+ [new \Exception()],
+ [[new \Exception()]],
+ [['test' => new \Exception()]],
+ [['test' => ['sub' => new \Exception()]]],
+ ];
+ }
+
+ /**
+ * @param mixed $configValue
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('checkTypeRecursivelyThrowsData')]
+ public function testCheckTypeRecursivelyThrows($configValue): void {
+ try {
+ $this->invokePrivate($this->command, 'checkTypeRecursively', [$configValue, 'name']);
+ $this->fail('Did not throw expected UnexpectedValueException');
+ } catch (\UnexpectedValueException $e) {
+ $this->assertStringStartsWith('Invalid system config value for "name"', $e->getMessage());
+ }
+ }
+
+ public static function validateArrayData(): array {
+ return [
+ [['system' => []]],
+ [['apps' => []]],
+ [['system' => [], 'apps' => []]],
+ ];
+ }
+
+ /**
+ * @param array $configArray
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('validateArrayData')]
+ public function testValidateArray($configArray): void {
+ $this->invokePrivate($this->command, 'validateArray', [$configArray]);
+ $this->assertTrue(true, 'Asserting that no exception is thrown');
+ }
+
+ public static function validateArrayThrowsData(): array {
+ return [
+ [[], 'At least one key of the following is expected:'],
+ [[0 => []], 'Found invalid entries in root'],
+ [['string' => []], 'Found invalid entries in root'],
+ ];
+ }
+
+ /**
+ *
+ * @param mixed $configArray
+ * @param string $expectedException
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('validateArrayThrowsData')]
+ public function testValidateArrayThrows($configArray, $expectedException): void {
+ try {
+ $this->invokePrivate($this->command, 'validateArray', [$configArray]);
+ $this->fail('Did not throw expected UnexpectedValueException');
+ } catch (\UnexpectedValueException $e) {
+ $this->assertStringStartsWith($expectedException, $e->getMessage());
+ }
+ }
+}
diff --git a/tests/Core/Command/Config/ListConfigsTest.php b/tests/Core/Command/Config/ListConfigsTest.php
new file mode 100644
index 00000000000..e4af55116c0
--- /dev/null
+++ b/tests/Core/Command/Config/ListConfigsTest.php
@@ -0,0 +1,328 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Config;
+
+use OC\Config\ConfigManager;
+use OC\Core\Command\Config\ListConfigs;
+use OC\SystemConfig;
+use OCP\IAppConfig;
+use OCP\IConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class ListConfigsTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $appConfig;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $systemConfig;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $configManager;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $systemConfig = $this->systemConfig = $this->getMockBuilder(SystemConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $appConfig = $this->appConfig = $this->getMockBuilder(IAppConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $configManager = $this->configManager = $this->getMockBuilder(ConfigManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var \OC\SystemConfig $systemConfig */
+ /** @var \OCP\IAppConfig $appConfig */
+ /** @var ConfigManager $configManager */
+ $this->command = new ListConfigs($systemConfig, $appConfig, $configManager);
+ }
+
+ public static function listData(): array {
+ return [
+ [
+ 'all',
+ // config.php
+ [
+ 'secret',
+ 'overwrite.cli.url',
+ ],
+ [
+ ['secret', 'N;', IConfig::SENSITIVE_VALUE],
+ ['overwrite.cli.url', 'N;', 'http://localhost'],
+ ],
+ // app config
+ [
+ ['files', [
+ 'enabled' => 'yes',
+ ]],
+ ['core', [
+ 'global_cache_gc_lastrun' => '1430388388',
+ ]],
+ ],
+ false,
+ json_encode([
+ 'system' => [
+ 'secret' => IConfig::SENSITIVE_VALUE,
+ 'overwrite.cli.url' => 'http://localhost',
+ ],
+ 'apps' => [
+ 'core' => [
+ 'global_cache_gc_lastrun' => '1430388388',
+ ],
+ 'files' => [
+ 'enabled' => 'yes',
+ ],
+ ],
+ ]),
+ ],
+ [
+ 'all',
+ // config.php
+ [
+ 'secret',
+ 'overwrite.cli.url',
+ ],
+ [
+ ['secret', 'N;', 'my secret'],
+ ['overwrite.cli.url', 'N;', 'http://localhost'],
+ ],
+ // app config
+ [
+ ['files', false, [
+ 'enabled' => 'yes',
+ ]],
+ ['core', false, [
+ 'global_cache_gc_lastrun' => '1430388388',
+ ]],
+ ],
+ true,
+ json_encode([
+ 'system' => [
+ 'secret' => 'my secret',
+ 'overwrite.cli.url' => 'http://localhost',
+ ],
+ 'apps' => [
+ 'core' => [
+ 'global_cache_gc_lastrun' => '1430388388',
+ ],
+ 'files' => [
+ 'enabled' => 'yes',
+ ],
+ ],
+ ]),
+ ],
+ [
+ 'system',
+ // config.php
+ [
+ 'secret',
+ 'objectstore',
+ 'overwrite.cli.url',
+ ],
+ [
+ ['secret', 'N;', IConfig::SENSITIVE_VALUE],
+ ['objectstore', 'N;', [
+ 'class' => 'OC\\Files\\ObjectStore\\Swift',
+ 'arguments' => [
+ 'username' => 'facebook100000123456789',
+ 'password' => IConfig::SENSITIVE_VALUE,
+ ],
+ ]],
+ ['overwrite.cli.url', 'N;', 'http://localhost'],
+ ],
+ // app config
+ [
+ ['files', [
+ 'enabled' => 'yes',
+ ]],
+ ['core', [
+ 'global_cache_gc_lastrun' => '1430388388',
+ ]],
+ ],
+ false,
+ json_encode([
+ 'system' => [
+ 'secret' => IConfig::SENSITIVE_VALUE,
+ 'objectstore' => [
+ 'class' => 'OC\\Files\\ObjectStore\\Swift',
+ 'arguments' => [
+ 'username' => 'facebook100000123456789',
+ 'password' => IConfig::SENSITIVE_VALUE,
+ ],
+ ],
+ 'overwrite.cli.url' => 'http://localhost',
+ ],
+ ]),
+ ],
+ [
+ 'system',
+ // config.php
+ [
+ 'secret',
+ 'overwrite.cli.url',
+ ],
+ [
+ ['secret', 'N;', 'my secret'],
+ ['overwrite.cli.url', 'N;', 'http://localhost'],
+ ],
+ // app config
+ [
+ ['files', [
+ 'enabled' => 'yes',
+ ]],
+ ['core', [
+ 'global_cache_gc_lastrun' => '1430388388',
+ ]],
+ ],
+ true,
+ json_encode([
+ 'system' => [
+ 'secret' => 'my secret',
+ 'overwrite.cli.url' => 'http://localhost',
+ ],
+ ]),
+ ],
+ [
+ 'files',
+ // config.php
+ [
+ 'secret',
+ 'overwrite.cli.url',
+ ],
+ [
+ ['secret', 'N;', 'my secret'],
+ ['overwrite.cli.url', 'N;', 'http://localhost'],
+ ],
+ // app config
+ [
+ ['files', [
+ 'enabled' => 'yes',
+ ]],
+ ['core', [
+ 'global_cache_gc_lastrun' => '1430388388',
+ ]],
+ ],
+ false,
+ json_encode([
+ 'apps' => [
+ 'files' => [
+ 'enabled' => 'yes',
+ ],
+ ],
+ ]),
+ ],
+ [
+ 'files',
+ // config.php
+ [
+ 'secret',
+ 'overwrite.cli.url',
+ ],
+ [
+ ['secret', 'N;', 'my secret'],
+ ['overwrite.cli.url', 'N;', 'http://localhost'],
+ ],
+ // app config
+ [
+ ['files', false, [
+ 'enabled' => 'yes',
+ ]],
+ ['core', false, [
+ 'global_cache_gc_lastrun' => '1430388388',
+ ]],
+ ],
+ true,
+ json_encode([
+ 'apps' => [
+ 'files' => [
+ 'enabled' => 'yes',
+ ],
+ ],
+ ]),
+ ],
+ ];
+ }
+
+ /**
+ *
+ * @param string $app
+ * @param array $systemConfigs
+ * @param array $systemConfigMap
+ * @param array $appConfig
+ * @param bool $private
+ * @param string $expected
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('listData')]
+ public function testList($app, $systemConfigs, $systemConfigMap, $appConfig, $private, $expected): void {
+ $this->systemConfig->expects($this->any())
+ ->method('getKeys')
+ ->willReturn($systemConfigs);
+ if ($private) {
+ $this->systemConfig->expects($this->any())
+ ->method('getValue')
+ ->willReturnMap($systemConfigMap);
+ $this->appConfig->expects($this->any())
+ ->method('getValues')
+ ->willReturnMap($appConfig);
+ } else {
+ $this->systemConfig->expects($this->any())
+ ->method('getFilteredValue')
+ ->willReturnMap($systemConfigMap);
+ $this->appConfig->expects($this->any())
+ ->method('getFilteredValues')
+ ->willReturnMap($appConfig);
+ }
+
+ $this->appConfig->expects($this->any())
+ ->method('getApps')
+ ->willReturn(['core', 'files']);
+ $this->appConfig->expects($this->any())
+ ->method('getValues')
+ ->willReturnMap($appConfig);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('app')
+ ->willReturn($app);
+
+ $this->consoleInput->expects($this->any())
+ ->method('getOption')
+ ->willReturnMap([
+ ['output', 'json'],
+ ['private', $private],
+ ]);
+
+ global $output;
+
+ $output = '';
+ $this->consoleOutput->expects($this->any())
+ ->method('writeln')
+ ->willReturnCallback(function ($value) {
+ global $output;
+ $output .= $value . "\n";
+ return $output;
+ });
+
+ $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+
+ $this->assertEquals($expected, trim($output, "\n"));
+ }
+}
diff --git a/tests/Core/Command/Config/System/CastHelperTest.php b/tests/Core/Command/Config/System/CastHelperTest.php
new file mode 100644
index 00000000000..924887daaf7
--- /dev/null
+++ b/tests/Core/Command/Config/System/CastHelperTest.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Core\Command\Config\System;
+
+use OC\Core\Command\Config\System\CastHelper;
+use Test\TestCase;
+
+class CastHelperTest extends TestCase {
+ private CastHelper $castHelper;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->castHelper = new CastHelper();
+ }
+
+ public static function castValueProvider(): array {
+ return [
+ [null, 'string', ['value' => '', 'readable-value' => 'empty string']],
+
+ ['abc', 'string', ['value' => 'abc', 'readable-value' => 'string abc']],
+
+ ['123', 'integer', ['value' => 123, 'readable-value' => 'integer 123']],
+ ['456', 'int', ['value' => 456, 'readable-value' => 'integer 456']],
+
+ ['2.25', 'double', ['value' => 2.25, 'readable-value' => 'double 2.25']],
+ ['0.5', 'float', ['value' => 0.5, 'readable-value' => 'double 0.5']],
+
+ ['', 'null', ['value' => null, 'readable-value' => 'null']],
+
+ ['true', 'boolean', ['value' => true, 'readable-value' => 'boolean true']],
+ ['false', 'bool', ['value' => false, 'readable-value' => 'boolean false']],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('castValueProvider')]
+ public function testCastValue($value, $type, $expectedValue): void {
+ $this->assertSame(
+ $expectedValue,
+ $this->castHelper->castValue($value, $type)
+ );
+ }
+
+ public static function castValueInvalidProvider(): array {
+ return [
+ ['123', 'foobar'],
+
+ [null, 'integer'],
+ ['abc', 'integer'],
+ ['76ggg', 'double'],
+ ['true', 'float'],
+ ['foobar', 'boolean'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('castValueInvalidProvider')]
+ public function testCastValueInvalid($value, $type): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ $this->castHelper->castValue($value, $type);
+ }
+}
diff --git a/tests/Core/Command/Config/System/DeleteConfigTest.php b/tests/Core/Command/Config/System/DeleteConfigTest.php
new file mode 100644
index 00000000000..b0a3580e1cd
--- /dev/null
+++ b/tests/Core/Command/Config/System/DeleteConfigTest.php
@@ -0,0 +1,207 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Config\System;
+
+use OC\Core\Command\Config\System\DeleteConfig;
+use OC\SystemConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DeleteConfigTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $systemConfig;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $systemConfig = $this->systemConfig = $this->getMockBuilder(SystemConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var SystemConfig $systemConfig */
+ $this->command = new DeleteConfig($systemConfig);
+ }
+
+ public static function deleteData(): array {
+ return [
+ [
+ 'name1',
+ true,
+ true,
+ 0,
+ 'info',
+ ],
+ [
+ 'name2',
+ true,
+ false,
+ 0,
+ 'info',
+ ],
+ [
+ 'name3',
+ false,
+ false,
+ 0,
+ 'info',
+ ],
+ [
+ 'name4',
+ false,
+ true,
+ 1,
+ 'error',
+ ],
+ ];
+ }
+
+ /**
+ *
+ * @param string $configName
+ * @param bool $configExists
+ * @param bool $checkIfExists
+ * @param int $expectedReturn
+ * @param string $expectedMessage
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('deleteData')]
+ public function testDelete($configName, $configExists, $checkIfExists, $expectedReturn, $expectedMessage): void {
+ $this->systemConfig->expects(($checkIfExists) ? $this->once() : $this->never())
+ ->method('getKeys')
+ ->willReturn($configExists ? [$configName] : []);
+
+ $this->systemConfig->expects(($expectedReturn === 0) ? $this->once() : $this->never())
+ ->method('deleteValue')
+ ->with($configName);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('name')
+ ->willReturn([$configName]);
+ $this->consoleInput->expects($this->any())
+ ->method('hasParameterOption')
+ ->with('--error-if-not-exists')
+ ->willReturn($checkIfExists);
+
+ $this->consoleOutput->expects($this->any())
+ ->method('writeln')
+ ->with($this->stringContains($expectedMessage));
+
+ $this->assertSame($expectedReturn, $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public static function deleteArrayData(): array {
+ return [
+ [
+ ['name', 'sub'],
+ true,
+ false,
+ true,
+ true,
+ 0,
+ 'info',
+ ],
+ [
+ ['name', 'sub', '2sub'],
+ true,
+ false,
+ ['sub' => ['2sub' => 1], 'sub2' => false],
+ ['sub' => [], 'sub2' => false],
+ 0,
+ 'info',
+ ],
+ [
+ ['name', 'sub3'],
+ true,
+ false,
+ ['sub' => ['2sub' => 1], 'sub2' => false],
+ ['sub' => ['2sub' => 1], 'sub2' => false],
+ 0,
+ 'info',
+ ],
+ [
+ ['name', 'sub'],
+ false,
+ true,
+ true,
+ true,
+ 1,
+ 'error',
+ ],
+ [
+ ['name', 'sub'],
+ true,
+ true,
+ true,
+ true,
+ 1,
+ 'error',
+ ],
+ [
+ ['name', 'sub3'],
+ true,
+ true,
+ ['sub' => ['2sub' => 1], 'sub2' => false],
+ ['sub' => ['2sub' => 1], 'sub2' => false],
+ 1,
+ 'error',
+ ],
+ ];
+ }
+
+ /**
+ *
+ * @param string[] $configNames
+ * @param bool $configKeyExists
+ * @param bool $checkIfKeyExists
+ * @param mixed $configValue
+ * @param mixed $updateValue
+ * @param int $expectedReturn
+ * @param string $expectedMessage
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('deleteArrayData')]
+ public function testArrayDelete(array $configNames, $configKeyExists, $checkIfKeyExists, $configValue, $updateValue, $expectedReturn, $expectedMessage): void {
+ $this->systemConfig->expects(($checkIfKeyExists) ? $this->once() : $this->never())
+ ->method('getKeys')
+ ->willReturn($configKeyExists ? [$configNames[0]] : []);
+
+ $this->systemConfig->expects(($configKeyExists) ? $this->once() : $this->never())
+ ->method('getValue')
+ ->willReturn($configValue);
+
+ $this->systemConfig->expects(($expectedReturn === 0) ? $this->once() : $this->never())
+ ->method('setValue')
+ ->with($configNames[0], $updateValue);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('name')
+ ->willReturn($configNames);
+ $this->consoleInput->expects($this->any())
+ ->method('hasParameterOption')
+ ->with('--error-if-not-exists')
+ ->willReturn($checkIfKeyExists);
+
+ $this->consoleOutput->expects($this->any())
+ ->method('writeln')
+ ->with($this->stringContains($expectedMessage));
+
+ $this->assertSame($expectedReturn, $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+}
diff --git a/tests/Core/Command/Config/System/GetConfigTest.php b/tests/Core/Command/Config/System/GetConfigTest.php
new file mode 100644
index 00000000000..8b84fd14198
--- /dev/null
+++ b/tests/Core/Command/Config/System/GetConfigTest.php
@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Config\System;
+
+use OC\Core\Command\Config\System\GetConfig;
+use OC\SystemConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class GetConfigTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $systemConfig;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $systemConfig = $this->systemConfig = $this->getMockBuilder(SystemConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var SystemConfig $systemConfig */
+ $this->command = new GetConfig($systemConfig);
+ }
+
+
+ public static function getData(): array {
+ return [
+ // String output as json
+ ['name', 'newvalue', true, null, false, 'json', 0, json_encode('newvalue')],
+ // String output as plain text
+ ['name', 'newvalue', true, null, false, 'plain', 0, 'newvalue'],
+ // String falling back to default output as json
+ ['name', null, false, 'newvalue', true, 'json', 0, json_encode('newvalue')],
+ // String falling back without default: error
+ ['name', null, false, null, false, 'json', 1, null],
+
+ // Int "0" output as json/plain
+ ['name', 0, true, null, false, 'json', 0, json_encode(0)],
+ ['name', 0, true, null, false, 'plain', 0, '0'],
+ // Int "1" output as json/plain
+ ['name', 1, true, null, false, 'json', 0, json_encode(1)],
+ ['name', 1, true, null, false, 'plain', 0, '1'],
+
+ // Bool "true" output as json/plain
+ ['name', true, true, null, false, 'json', 0, json_encode(true)],
+ ['name', true, true, null, false, 'plain', 0, 'true'],
+ // Bool "false" output as json/plain
+ ['name', false, true, null, false, 'json', 0, json_encode(false)],
+ ['name', false, true, null, false, 'plain', 0, 'false'],
+
+ // Null output as json/plain
+ ['name', null, true, null, false, 'json', 0, json_encode(null)],
+ ['name', null, true, null, false, 'plain', 0, 'null'],
+
+ // Array output as json/plain
+ ['name', ['a', 'b'], true, null, false, 'json', 0, json_encode(['a', 'b'])],
+ ['name', ['a', 'b'], true, null, false, 'plain', 0, "a\nb"],
+ // Key array output as json/plain
+ ['name', [0 => 'a', 1 => 'b'], true, null, false, 'json', 0, json_encode(['a', 'b'])],
+ ['name', [0 => 'a', 1 => 'b'], true, null, false, 'plain', 0, "a\nb"],
+ // Associative array output as json/plain
+ ['name', ['a' => 1, 'b' => 2], true, null, false, 'json', 0, json_encode(['a' => 1, 'b' => 2])],
+ ['name', ['a' => 1, 'b' => 2], true, null, false, 'plain', 0, "a: 1\nb: 2"],
+
+ // Nested depth
+ [['name', 'a'], ['a' => 1, 'b' => 2], true, null, false, 'json', 0, json_encode(1)],
+ [['name', 'a'], ['a' => 1, 'b' => 2], true, null, false, 'plain', 0, '1'],
+ [['name', 'c'], ['a' => 1, 'b' => 2], true, true, true, 'json', 0, json_encode(true)],
+ [['name', 'c'], ['a' => 1, 'b' => 2], true, true, false, 'json', 1, null],
+
+ ];
+ }
+
+ /**
+ *
+ * @param string[] $configNames
+ * @param mixed $value
+ * @param bool $configExists
+ * @param mixed $defaultValue
+ * @param bool $hasDefault
+ * @param string $outputFormat
+ * @param int $expectedReturn
+ * @param string $expectedMessage
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('getData')]
+ public function testGet($configNames, $value, $configExists, $defaultValue, $hasDefault, $outputFormat, $expectedReturn, $expectedMessage): void {
+ if (is_array($configNames)) {
+ $configName = $configNames[0];
+ } else {
+ $configName = $configNames;
+ $configNames = [$configName];
+ }
+ $this->systemConfig->expects($this->atLeastOnce())
+ ->method('getKeys')
+ ->willReturn($configExists ? [$configName] : []);
+
+ if (!$expectedReturn) {
+ if ($configExists) {
+ $this->systemConfig->expects($this->once())
+ ->method('getValue')
+ ->with($configName)
+ ->willReturn($value);
+ }
+ }
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('name')
+ ->willReturn($configNames);
+ $this->consoleInput->expects($this->any())
+ ->method('getOption')
+ ->willReturnMap([
+ ['default-value', $defaultValue],
+ ['output', $outputFormat],
+ ]);
+ $this->consoleInput->expects($this->any())
+ ->method('hasParameterOption')
+ ->willReturnMap([
+ ['--output', false, true],
+ ['--default-value', false,$hasDefault],
+ ]);
+
+ if ($expectedMessage !== null) {
+ global $output;
+
+ $output = '';
+ $this->consoleOutput->expects($this->any())
+ ->method('writeln')
+ ->willReturnCallback(function ($value) {
+ global $output;
+ $output .= $value . "\n";
+ return $output;
+ });
+ }
+
+ $this->assertSame($expectedReturn, $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+
+ if ($expectedMessage !== null) {
+ global $output;
+ // Remove the trailing newline
+ $this->assertSame($expectedMessage, substr($output, 0, -1));
+ }
+ }
+}
diff --git a/tests/Core/Command/Config/System/SetConfigTest.php b/tests/Core/Command/Config/System/SetConfigTest.php
new file mode 100644
index 00000000000..a99b832c160
--- /dev/null
+++ b/tests/Core/Command/Config/System/SetConfigTest.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Config\System;
+
+use OC\Core\Command\Config\System\CastHelper;
+use OC\Core\Command\Config\System\SetConfig;
+use OC\SystemConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class SetConfigTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $systemConfig;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $systemConfig = $this->systemConfig = $this->getMockBuilder(SystemConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var SystemConfig $systemConfig */
+ $this->command = new SetConfig($systemConfig, new CastHelper());
+ }
+
+
+ public static function dataTest() {
+ return [
+ [['name'], 'newvalue', null, 'newvalue'],
+ [['a', 'b', 'c'], 'foobar', null, ['b' => ['c' => 'foobar']]],
+ [['a', 'b', 'c'], 'foobar', ['b' => ['d' => 'barfoo']], ['b' => ['d' => 'barfoo', 'c' => 'foobar']]],
+ ];
+ }
+
+ /**
+ *
+ * @param array $configNames
+ * @param string $newValue
+ * @param mixed $existingData
+ * @param mixed $expectedValue
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTest')]
+ public function testSet($configNames, $newValue, $existingData, $expectedValue): void {
+ $this->systemConfig->expects($this->once())
+ ->method('setValue')
+ ->with($configNames[0], $expectedValue);
+ $this->systemConfig->method('getValue')
+ ->with($configNames[0])
+ ->willReturn($existingData);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('name')
+ ->willReturn($configNames);
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['value', $newValue],
+ ['type', 'string'],
+ ]);
+
+ $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public static function setUpdateOnlyProvider(): array {
+ return [
+ [['name'], null],
+ [['a', 'b', 'c'], null],
+ [['a', 'b', 'c'], ['b' => 'foobar']],
+ [['a', 'b', 'c'], ['b' => ['d' => 'foobar']]],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('setUpdateOnlyProvider')]
+ public function testSetUpdateOnly($configNames, $existingData): void {
+ $this->expectException(\UnexpectedValueException::class);
+
+ $this->systemConfig->expects($this->never())
+ ->method('setValue');
+ $this->systemConfig->method('getValue')
+ ->with($configNames[0])
+ ->willReturn($existingData);
+ $this->systemConfig->method('getKeys')
+ ->willReturn($existingData ? $configNames[0] : []);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('name')
+ ->willReturn($configNames);
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['value', 'foobar'],
+ ['type', 'string'],
+ ['update-only', true],
+ ]);
+
+ $this->invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Encryption/ChangeKeyStorageRootTest.php b/tests/Core/Command/Encryption/ChangeKeyStorageRootTest.php
new file mode 100644
index 00000000000..0bc6cbb64cf
--- /dev/null
+++ b/tests/Core/Command/Encryption/ChangeKeyStorageRootTest.php
@@ -0,0 +1,360 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Encryption;
+
+use OC\Core\Command\Encryption\ChangeKeyStorageRoot;
+use OC\Encryption\Keys\Storage;
+use OC\Encryption\Util;
+use OC\Files\View;
+use OCP\IConfig;
+use OCP\IUserManager;
+use OCP\UserInterface;
+use Symfony\Component\Console\Formatter\OutputFormatterInterface;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class ChangeKeyStorageRootTest extends TestCase {
+ /** @var ChangeKeyStorageRoot */
+ protected $changeKeyStorageRoot;
+
+ /** @var View | \PHPUnit\Framework\MockObject\MockObject */
+ protected $view;
+
+ /** @var IUserManager | \PHPUnit\Framework\MockObject\MockObject */
+ protected $userManager;
+
+ /** @var IConfig | \PHPUnit\Framework\MockObject\MockObject */
+ protected $config;
+
+ /** @var Util | \PHPUnit\Framework\MockObject\MockObject */
+ protected $util;
+
+ /** @var QuestionHelper | \PHPUnit\Framework\MockObject\MockObject */
+ protected $questionHelper;
+
+ /** @var InputInterface | \PHPUnit\Framework\MockObject\MockObject */
+ protected $inputInterface;
+
+ /** @var OutputInterface | \PHPUnit\Framework\MockObject\MockObject */
+ protected $outputInterface;
+
+ /** @var UserInterface|\PHPUnit\Framework\MockObject\MockObject */
+ protected $userInterface;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->view = $this->getMockBuilder(View::class)->getMock();
+ $this->userManager = $this->getMockBuilder(IUserManager::class)->getMock();
+ $this->config = $this->getMockBuilder(IConfig::class)->getMock();
+ $this->util = $this->getMockBuilder('OC\Encryption\Util')->disableOriginalConstructor()->getMock();
+ $this->questionHelper = $this->getMockBuilder(QuestionHelper::class)->getMock();
+ $this->inputInterface = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->outputInterface = $this->getMockBuilder(OutputInterface::class)->getMock();
+ $this->userInterface = $this->getMockBuilder(UserInterface::class)->getMock();
+
+ /* We need format method to return a string */
+ $outputFormatter = $this->createMock(OutputFormatterInterface::class);
+ $outputFormatter->method('isDecorated')->willReturn(false);
+ $outputFormatter->method('format')->willReturnArgument(0);
+
+ $this->outputInterface->expects($this->any())->method('getFormatter')
+ ->willReturn($outputFormatter);
+
+ $this->changeKeyStorageRoot = new ChangeKeyStorageRoot(
+ $this->view,
+ $this->userManager,
+ $this->config,
+ $this->util,
+ $this->questionHelper
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestExecute')]
+ public function testExecute($newRoot, $answer, $successMoveKey): void {
+ $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot')
+ ->setConstructorArgs(
+ [
+ $this->view,
+ $this->userManager,
+ $this->config,
+ $this->util,
+ $this->questionHelper
+ ]
+ )->onlyMethods(['moveAllKeys'])->getMock();
+
+ $this->util->expects($this->once())->method('getKeyStorageRoot')
+ ->willReturn('');
+ $this->inputInterface->expects($this->once())->method('getArgument')
+ ->with('newRoot')->willReturn($newRoot);
+
+ if ($answer === true || $newRoot !== null) {
+ $changeKeyStorageRoot->expects($this->once())->method('moveAllKeys')
+ ->willReturn($successMoveKey);
+ } else {
+ $changeKeyStorageRoot->expects($this->never())->method('moveAllKeys');
+ }
+
+ if ($successMoveKey === true) {
+ $this->util->expects($this->once())->method('setKeyStorageRoot');
+ } else {
+ $this->util->expects($this->never())->method('setKeyStorageRoot');
+ }
+
+ if ($newRoot === null) {
+ $this->questionHelper->expects($this->once())->method('ask')->willReturn($answer);
+ } else {
+ $this->questionHelper->expects($this->never())->method('ask');
+ }
+
+ $this->invokePrivate(
+ $changeKeyStorageRoot,
+ 'execute',
+ [$this->inputInterface, $this->outputInterface]
+ );
+ }
+
+ public static function dataTestExecute(): array {
+ return [
+ [null, true, true],
+ [null, true, false],
+ [null, false, null],
+ ['/newRoot', null, true],
+ ['/newRoot', null, false]
+ ];
+ }
+
+ public function testMoveAllKeys(): void {
+ /** @var ChangeKeyStorageRoot $changeKeyStorageRoot */
+ $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot')
+ ->setConstructorArgs(
+ [
+ $this->view,
+ $this->userManager,
+ $this->config,
+ $this->util,
+ $this->questionHelper
+ ]
+ )->onlyMethods(['prepareNewRoot', 'moveSystemKeys', 'moveUserKeys'])->getMock();
+
+ $changeKeyStorageRoot->expects($this->once())->method('prepareNewRoot')->with('newRoot');
+ $changeKeyStorageRoot->expects($this->once())->method('moveSystemKeys')->with('oldRoot', 'newRoot');
+ $changeKeyStorageRoot->expects($this->once())->method('moveUserKeys')->with('oldRoot', 'newRoot', $this->outputInterface);
+
+ $this->invokePrivate($changeKeyStorageRoot, 'moveAllKeys', ['oldRoot', 'newRoot', $this->outputInterface]);
+ }
+
+ public function testPrepareNewRoot(): void {
+ $this->view->expects($this->once())->method('is_dir')->with('newRoot')
+ ->willReturn(true);
+
+ $this->view->expects($this->once())->method('file_put_contents')
+ ->with('newRoot/' . Storage::KEY_STORAGE_MARKER,
+ 'Nextcloud will detect this folder as key storage root only if this file exists')->willReturn(true);
+
+ $this->invokePrivate($this->changeKeyStorageRoot, 'prepareNewRoot', ['newRoot']);
+ }
+
+ /**
+ *
+ * @param bool $dirExists
+ * @param bool $couldCreateFile
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestPrepareNewRootException')]
+ public function testPrepareNewRootException($dirExists, $couldCreateFile): void {
+ $this->expectException(\Exception::class);
+
+ $this->view->expects($this->once())->method('is_dir')->with('newRoot')
+ ->willReturn($dirExists);
+ $this->view->expects($this->any())->method('file_put_contents')->willReturn($couldCreateFile);
+
+ $this->invokePrivate($this->changeKeyStorageRoot, 'prepareNewRoot', ['newRoot']);
+ }
+
+ public static function dataTestPrepareNewRootException(): array {
+ return [
+ [true, false],
+ [true, null],
+ [false, true]
+ ];
+ }
+
+ /**
+ *
+ * @param bool $dirExists
+ * @param bool $targetExists
+ * @param bool $executeRename
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveSystemKeys')]
+ public function testMoveSystemKeys($dirExists, $targetExists, $executeRename): void {
+ $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot')
+ ->setConstructorArgs(
+ [
+ $this->view,
+ $this->userManager,
+ $this->config,
+ $this->util,
+ $this->questionHelper
+ ]
+ )->onlyMethods(['targetExists'])->getMock();
+
+ $this->view->expects($this->once())->method('is_dir')
+ ->with('oldRoot/files_encryption')->willReturn($dirExists);
+ $changeKeyStorageRoot->expects($this->any())->method('targetExists')
+ ->with('newRoot/files_encryption')->willReturn($targetExists);
+
+ if ($executeRename) {
+ $this->view->expects($this->once())->method('rename')
+ ->with('oldRoot/files_encryption', 'newRoot/files_encryption');
+ } else {
+ $this->view->expects($this->never())->method('rename');
+ }
+
+ $this->invokePrivate($changeKeyStorageRoot, 'moveSystemKeys', ['oldRoot', 'newRoot']);
+ }
+
+ public static function dataTestMoveSystemKeys(): array {
+ return [
+ [true, false, true],
+ [false, true, false],
+ [true, true, false],
+ [false, false, false]
+ ];
+ }
+
+
+ public function testMoveUserKeys(): void {
+ $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot')
+ ->setConstructorArgs(
+ [
+ $this->view,
+ $this->userManager,
+ $this->config,
+ $this->util,
+ $this->questionHelper
+ ]
+ )->onlyMethods(['setupUserFS', 'moveUserEncryptionFolder'])->getMock();
+
+ $this->userManager->expects($this->once())->method('getBackends')
+ ->willReturn([$this->userInterface]);
+ $this->userInterface->expects($this->once())->method('getUsers')
+ ->willReturn(['user1', 'user2']);
+ $changeKeyStorageRoot->expects($this->exactly(2))->method('setupUserFS');
+ $changeKeyStorageRoot->expects($this->exactly(2))->method('moveUserEncryptionFolder');
+
+ $this->invokePrivate($changeKeyStorageRoot, 'moveUserKeys', ['oldRoot', 'newRoot', $this->outputInterface]);
+ }
+
+ /**
+ *
+ * @param bool $userExists
+ * @param bool $isDir
+ * @param bool $targetExists
+ * @param bool $shouldRename
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestMoveUserEncryptionFolder')]
+ public function testMoveUserEncryptionFolder($userExists, $isDir, $targetExists, $shouldRename): void {
+ $changeKeyStorageRoot = $this->getMockBuilder('OC\Core\Command\Encryption\ChangeKeyStorageRoot')
+ ->setConstructorArgs(
+ [
+ $this->view,
+ $this->userManager,
+ $this->config,
+ $this->util,
+ $this->questionHelper
+ ]
+ )->onlyMethods(['targetExists', 'prepareParentFolder'])->getMock();
+
+ $this->userManager->expects($this->once())->method('userExists')
+ ->willReturn($userExists);
+ $this->view->expects($this->any())->method('is_dir')
+ ->willReturn($isDir);
+ $changeKeyStorageRoot->expects($this->any())->method('targetExists')
+ ->willReturn($targetExists);
+
+ if ($shouldRename) {
+ $changeKeyStorageRoot->expects($this->once())->method('prepareParentFolder')
+ ->with('newRoot/user1');
+ $this->view->expects($this->once())->method('rename')
+ ->with('oldRoot/user1/files_encryption', 'newRoot/user1/files_encryption');
+ } else {
+ $changeKeyStorageRoot->expects($this->never())->method('prepareParentFolder');
+ $this->view->expects($this->never())->method('rename');
+ }
+
+ $this->invokePrivate($changeKeyStorageRoot, 'moveUserEncryptionFolder', ['user1', 'oldRoot', 'newRoot']);
+ }
+
+ public static function dataTestMoveUserEncryptionFolder(): array {
+ return [
+ [true, true, false, true],
+ [true, false, true, false],
+ [false, true, true, false],
+ [false, false, true, false],
+ [false, true, false, false],
+ [false, true, true, false],
+ [false, false, false, false]
+ ];
+ }
+
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestPrepareParentFolder')]
+ public function testPrepareParentFolder($path, $pathExists): void {
+ $this->view->expects($this->any())->method('file_exists')
+ ->willReturnCallback(
+ function ($fileExistsPath) use ($path, $pathExists) {
+ if ($path === $fileExistsPath) {
+ return $pathExists;
+ }
+ return false;
+ }
+ );
+
+ if ($pathExists === false) {
+ $subDirs = explode('/', ltrim($path, '/'));
+ $this->view->expects($this->exactly(count($subDirs)))->method('mkdir');
+ } else {
+ $this->view->expects($this->never())->method('mkdir');
+ }
+
+ $this->invokePrivate(
+ $this->changeKeyStorageRoot,
+ 'prepareParentFolder',
+ [$path]
+ );
+ }
+
+ public static function dataTestPrepareParentFolder(): array {
+ return [
+ ['/user/folder/sub_folder/keystorage', true],
+ ['/user/folder/sub_folder/keystorage', false]
+ ];
+ }
+
+ public function testTargetExists(): void {
+ $this->view->expects($this->once())->method('file_exists')->with('path')
+ ->willReturn(false);
+
+ $this->assertFalse(
+ $this->invokePrivate($this->changeKeyStorageRoot, 'targetExists', ['path'])
+ );
+ }
+
+
+ public function testTargetExistsException(): void {
+ $this->expectException(\Exception::class);
+
+ $this->view->expects($this->once())->method('file_exists')->with('path')
+ ->willReturn(true);
+
+ $this->invokePrivate($this->changeKeyStorageRoot, 'targetExists', ['path']);
+ }
+}
diff --git a/tests/Core/Command/Encryption/DecryptAllTest.php b/tests/Core/Command/Encryption/DecryptAllTest.php
new file mode 100644
index 00000000000..41d9e4c713f
--- /dev/null
+++ b/tests/Core/Command/Encryption/DecryptAllTest.php
@@ -0,0 +1,216 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Encryption;
+
+use OC\Core\Command\Encryption\DecryptAll;
+use OCP\App\IAppManager;
+use OCP\Encryption\IManager;
+use OCP\IConfig;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DecryptAllTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject|IConfig */
+ protected $config;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject | \OCP\Encryption\IManager */
+ protected $encryptionManager;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject|IAppManager */
+ protected $appManager;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject | \Symfony\Component\Console\Input\InputInterface */
+ protected $consoleInput;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject | \Symfony\Component\Console\Output\OutputInterface */
+ protected $consoleOutput;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject | \Symfony\Component\Console\Helper\QuestionHelper */
+ protected $questionHelper;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject | \OC\Encryption\DecryptAll */
+ protected $decryptAll;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->getMockBuilder(IConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->encryptionManager = $this->getMockBuilder(IManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->appManager = $this->getMockBuilder(IAppManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->questionHelper = $this->getMockBuilder(QuestionHelper::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->decryptAll = $this->getMockBuilder(\OC\Encryption\DecryptAll::class)
+ ->disableOriginalConstructor()->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleInput->expects($this->any())
+ ->method('isInteractive')
+ ->willReturn(true);
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ $this->config->expects($this->any())
+ ->method('getSystemValue')
+ ->with('maintenance', false)
+ ->willReturn(false);
+ $this->appManager->expects($this->any())
+ ->method('isEnabledForUser')
+ ->with('files_trashbin')->willReturn(true);
+ }
+
+ public function testMaintenanceAndTrashbin(): void {
+ // on construct we enable single-user-mode and disable the trash bin
+ // on destruct we disable single-user-mode again and enable the trash bin
+ $calls = [
+ ['maintenance', true],
+ ['maintenance', false],
+ ];
+ $this->config->expects($this->exactly(2))
+ ->method('setSystemValue')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $this->appManager->expects($this->once())
+ ->method('disableApp')
+ ->with('files_trashbin');
+ $this->appManager->expects($this->once())
+ ->method('enableApp')
+ ->with('files_trashbin');
+
+ $instance = new DecryptAll(
+ $this->encryptionManager,
+ $this->appManager,
+ $this->config,
+ $this->decryptAll,
+ $this->questionHelper
+ );
+ $this->invokePrivate($instance, 'forceMaintenanceAndTrashbin');
+
+ $this->assertTrue(
+ $this->invokePrivate($instance, 'wasTrashbinEnabled')
+ );
+
+ $this->assertFalse(
+ $this->invokePrivate($instance, 'wasMaintenanceModeEnabled')
+ );
+ $this->invokePrivate($instance, 'resetMaintenanceAndTrashbin');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestExecute')]
+ public function testExecute($encryptionEnabled, $continue): void {
+ $instance = new DecryptAll(
+ $this->encryptionManager,
+ $this->appManager,
+ $this->config,
+ $this->decryptAll,
+ $this->questionHelper
+ );
+
+ $this->encryptionManager->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn($encryptionEnabled);
+
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->with('user')
+ ->willReturn('user1');
+
+ if ($encryptionEnabled) {
+ $calls = [
+ ['core', 'encryption_enabled', 'no'],
+ ['core', 'encryption_enabled', 'yes'],
+ ];
+ $this->config->expects($this->exactly(2))
+ ->method('setAppValue')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $this->questionHelper->expects($this->once())
+ ->method('ask')
+ ->willReturn($continue);
+ if ($continue) {
+ $this->decryptAll->expects($this->once())
+ ->method('decryptAll')
+ ->with($this->consoleInput, $this->consoleOutput, 'user1');
+ } else {
+ $this->decryptAll->expects($this->never())->method('decryptAll');
+ }
+ } else {
+ $this->config->expects($this->never())->method('setAppValue');
+ $this->decryptAll->expects($this->never())->method('decryptAll');
+ $this->questionHelper->expects($this->never())->method('ask');
+ }
+
+ $this->invokePrivate($instance, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public static function dataTestExecute(): array {
+ return [
+ [true, true],
+ [true, false],
+ [false, true],
+ [false, false]
+ ];
+ }
+
+
+ public function testExecuteFailure(): void {
+ $this->expectException(\Exception::class);
+
+ $instance = new DecryptAll(
+ $this->encryptionManager,
+ $this->appManager,
+ $this->config,
+ $this->decryptAll,
+ $this->questionHelper
+ );
+
+ // make sure that we enable encryption again after a exception was thrown
+ $calls = [
+ ['core', 'encryption_enabled', 'no'],
+ ['core', 'encryption_enabled', 'yes'],
+ ];
+ $this->config->expects($this->exactly(2))
+ ->method('setAppValue')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $this->encryptionManager->expects($this->once())
+ ->method('isEnabled')
+ ->willReturn(true);
+
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->with('user')
+ ->willReturn('user1');
+
+ $this->questionHelper->expects($this->once())
+ ->method('ask')
+ ->willReturn(true);
+
+ $this->decryptAll->expects($this->once())
+ ->method('decryptAll')
+ ->with($this->consoleInput, $this->consoleOutput, 'user1')
+ ->willReturnCallback(function (): void {
+ throw new \Exception();
+ });
+
+ $this->invokePrivate($instance, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Encryption/DisableTest.php b/tests/Core/Command/Encryption/DisableTest.php
new file mode 100644
index 00000000000..a89fd636e47
--- /dev/null
+++ b/tests/Core/Command/Encryption/DisableTest.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Encryption;
+
+use OC\Core\Command\Encryption\Disable;
+use OCP\IConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DisableTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $config;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $config = $this->config = $this->getMockBuilder(IConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var IConfig $config */
+ $this->command = new Disable($config);
+ }
+
+
+ public static function dataDisable(): array {
+ return [
+ ['yes', true, 'Encryption disabled'],
+ ['no', false, 'Encryption is already disabled'],
+ ];
+ }
+
+ /**
+ *
+ * @param string $oldStatus
+ * @param bool $isUpdating
+ * @param string $expectedString
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataDisable')]
+ public function testDisable($oldStatus, $isUpdating, $expectedString): void {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->with('core', 'encryption_enabled', $this->anything())
+ ->willReturn($oldStatus);
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains($expectedString));
+
+ if ($isUpdating) {
+ $this->config->expects($this->once())
+ ->method('setAppValue')
+ ->with('core', 'encryption_enabled', 'no');
+ }
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Encryption/EnableTest.php b/tests/Core/Command/Encryption/EnableTest.php
new file mode 100644
index 00000000000..32d1a7576f5
--- /dev/null
+++ b/tests/Core/Command/Encryption/EnableTest.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Encryption;
+
+use OC\Core\Command\Encryption\Enable;
+use OCP\Encryption\IManager;
+use OCP\IConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class EnableTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $config;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $manager;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $config = $this->config = $this->getMockBuilder(IConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $manager = $this->manager = $this->getMockBuilder(IManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var \OCP\IConfig $config */
+ /** @var \OCP\Encryption\IManager $manager */
+ $this->command = new Enable($config, $manager);
+ }
+
+
+ public static function dataEnable(): array {
+ return [
+ ['no', null, [], true, 'Encryption enabled', 'No encryption module is loaded'],
+ ['yes', null, [], false, 'Encryption is already enabled', 'No encryption module is loaded'],
+ ['no', null, ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'No default module is set'],
+ ['no', 'OC_NO_MODULE', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'The current default module does not exist: OC_NO_MODULE'],
+ ['no', 'OC_TEST_MODULE', ['OC_TEST_MODULE' => []], true, 'Encryption enabled', 'Default module: OC_TEST_MODULE'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataEnable')]
+ public function testEnable(string $oldStatus, ?string $defaultModule, array $availableModules, bool $isUpdating, string $expectedString, string $expectedDefaultModuleString): void {
+ if ($isUpdating) {
+ $this->config->expects($this->once())
+ ->method('setAppValue')
+ ->with('core', 'encryption_enabled', 'yes');
+ }
+
+ $this->manager->expects($this->atLeastOnce())
+ ->method('getEncryptionModules')
+ ->willReturn($availableModules);
+
+ if (empty($availableModules)) {
+ $this->config->expects($this->once())
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'encryption_enabled', 'no', $oldStatus],
+ ]);
+ } else {
+ $this->config->expects($this->exactly(2))
+ ->method('getAppValue')
+ ->willReturnMap([
+ ['core', 'encryption_enabled', 'no', $oldStatus],
+ ['core', 'default_encryption_module', null, $defaultModule],
+ ]);
+ }
+
+ $calls = [
+ [$expectedString, 0],
+ ['', 0],
+ [$expectedDefaultModuleString, 0],
+ ];
+ $this->consoleOutput->expects($this->exactly(3))
+ ->method('writeln')
+ ->willReturnCallback(function (string $message, int $level) use (&$calls): void {
+ $call = array_shift($calls);
+ $this->assertStringContainsString($call[0], $message);
+ $this->assertSame($call[1], $level);
+ });
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Encryption/EncryptAllTest.php b/tests/Core/Command/Encryption/EncryptAllTest.php
new file mode 100644
index 00000000000..15cbe83737d
--- /dev/null
+++ b/tests/Core/Command/Encryption/EncryptAllTest.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Encryption;
+
+use OC\Core\Command\Encryption\EncryptAll;
+use OCP\App\IAppManager;
+use OCP\Encryption\IEncryptionModule;
+use OCP\Encryption\IManager;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class EncryptAllTest extends TestCase {
+ private IConfig&MockObject $config;
+ private IManager&MockObject $encryptionManager;
+ private IAppManager&MockObject $appManager;
+ private InputInterface&MockObject $consoleInput;
+ private OutputInterface&MockObject $consoleOutput;
+ private QuestionHelper&MockObject $questionHelper;
+ private IEncryptionModule&MockObject $encryptionModule;
+
+ private EncryptAll $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->encryptionManager = $this->createMock(IManager::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->encryptionModule = $this->createMock(IEncryptionModule::class);
+ $this->questionHelper = $this->createMock(QuestionHelper::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleInput->expects($this->any())
+ ->method('isInteractive')
+ ->willReturn(true);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+ }
+
+ public function testEncryptAll(): void {
+ // trash bin needs to be disabled in order to avoid adding dummy files to the users
+ // trash bin which gets deleted during the encryption process
+ $this->appManager->expects($this->once())->method('disableApp')->with('files_trashbin');
+
+ $instance = new EncryptAll($this->encryptionManager, $this->appManager, $this->config, $this->questionHelper);
+ $this->invokePrivate($instance, 'forceMaintenanceAndTrashbin');
+ $this->invokePrivate($instance, 'resetMaintenanceAndTrashbin');
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTestExecute')]
+ public function testExecute($answer, $askResult): void {
+ $command = new EncryptAll($this->encryptionManager, $this->appManager, $this->config, $this->questionHelper);
+
+ $this->encryptionManager->expects($this->once())->method('isEnabled')->willReturn(true);
+ $this->questionHelper->expects($this->once())->method('ask')->willReturn($askResult);
+
+ if ($answer === 'Y' || $answer === 'y') {
+ $this->encryptionManager->expects($this->once())
+ ->method('getEncryptionModule')->willReturn($this->encryptionModule);
+ $this->encryptionModule->expects($this->once())
+ ->method('encryptAll')->with($this->consoleInput, $this->consoleOutput);
+ } else {
+ $this->encryptionManager->expects($this->never())->method('getEncryptionModule');
+ $this->encryptionModule->expects($this->never())->method('encryptAll');
+ }
+
+ $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public static function dataTestExecute(): array {
+ return [
+ ['y', true], ['Y', true], ['n', false], ['N', false], ['', false]
+ ];
+ }
+
+
+ public function testExecuteException(): void {
+ $this->expectException(\Exception::class);
+
+ $command = new EncryptAll($this->encryptionManager, $this->appManager, $this->config, $this->questionHelper);
+ $this->encryptionManager->expects($this->once())->method('isEnabled')->willReturn(false);
+ $this->encryptionManager->expects($this->never())->method('getEncryptionModule');
+ $this->encryptionModule->expects($this->never())->method('encryptAll');
+ $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Encryption/SetDefaultModuleTest.php b/tests/Core/Command/Encryption/SetDefaultModuleTest.php
new file mode 100644
index 00000000000..df38d730db3
--- /dev/null
+++ b/tests/Core/Command/Encryption/SetDefaultModuleTest.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Encryption;
+
+use OC\Core\Command\Encryption\SetDefaultModule;
+use OCP\Encryption\IManager;
+use OCP\IConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class SetDefaultModuleTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject|IManager */
+ protected $manager;
+ /** @var \PHPUnit\Framework\MockObject\MockObject|IConfig */
+ protected $config;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->manager = $this->getMockBuilder(IManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->config = $this->getMockBuilder(IConfig::class)
+ ->getMock();
+
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ $this->command = new SetDefaultModule($this->manager, $this->config);
+ }
+
+
+ public static function dataSetDefaultModule(): array {
+ return [
+ ['ID0', 'ID0', null, null, 'already'],
+ ['ID0', 'ID1', 'ID1', true, 'info'],
+ ['ID0', 'ID1', 'ID1', false, 'error'],
+ ];
+ }
+
+ /**
+ *
+ * @param string $oldModule
+ * @param string $newModule
+ * @param string $updateModule
+ * @param bool $updateSuccess
+ * @param string $expectedString
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSetDefaultModule')]
+ public function testSetDefaultModule($oldModule, $newModule, $updateModule, $updateSuccess, $expectedString): void {
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('module')
+ ->willReturn($newModule);
+
+ $this->manager->expects($this->once())
+ ->method('getDefaultEncryptionModuleId')
+ ->willReturn($oldModule);
+
+ $this->config->expects($this->once())
+ ->method('getSystemValue')
+ ->with('maintenance', false)
+ ->willReturn(false);
+
+ if ($updateModule) {
+ $this->manager->expects($this->once())
+ ->method('setDefaultEncryptionModule')
+ ->with($updateModule)
+ ->willReturn($updateSuccess);
+ }
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains($expectedString));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ /**
+ *
+ * @param string $oldModule
+ * @param string $newModule
+ * @param string $updateModule
+ * @param bool $updateSuccess
+ * @param string $expectedString
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataSetDefaultModule')]
+ public function testMaintenanceMode($oldModule, $newModule, $updateModule, $updateSuccess, $expectedString): void {
+ $this->consoleInput->expects($this->never())
+ ->method('getArgument')
+ ->with('module')
+ ->willReturn($newModule);
+
+ $this->manager->expects($this->never())
+ ->method('getDefaultEncryptionModuleId')
+ ->willReturn($oldModule);
+
+ $this->config->expects($this->once())
+ ->method('getSystemValue')
+ ->with('maintenance', false)
+ ->willReturn(true);
+
+ $calls = [
+ 'Maintenance mode must be disabled when setting default module,',
+ 'in order to load the relevant encryption modules correctly.',
+ ];
+ $this->consoleOutput->expects($this->exactly(2))
+ ->method('writeln')
+ ->willReturnCallback(function ($message) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertStringContainsString($expected, $message);
+ });
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Group/AddTest.php b/tests/Core/Command/Group/AddTest.php
new file mode 100644
index 00000000000..24f2d823292
--- /dev/null
+++ b/tests/Core/Command/Group/AddTest.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\Group;
+
+use OC\Core\Command\Group\Add;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class AddTest extends TestCase {
+ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $groupManager;
+
+ /** @var Add */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->command = new Add($this->groupManager);
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) {
+ if ($arg === 'groupid') {
+ return 'myGroup';
+ }
+ throw new \Exception();
+ });
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testGroupExists(): void {
+ $gid = 'myGroup';
+ $group = $this->createMock(IGroup::class);
+ $this->groupManager->method('get')
+ ->with($gid)
+ ->willReturn($group);
+
+ $this->groupManager->expects($this->never())
+ ->method('createGroup');
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with($this->equalTo('<error>Group "' . $gid . '" already exists.</error>'));
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testAdd(): void {
+ $gid = 'myGroup';
+ $group = $this->createMock(IGroup::class);
+ $group->method('getGID')
+ ->willReturn($gid);
+ $this->groupManager->method('createGroup')
+ ->willReturn($group);
+
+ $this->groupManager->expects($this->once())
+ ->method('createGroup')
+ ->with($this->equalTo($gid));
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with($this->equalTo('Created group "' . $group->getGID() . '"'));
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/Group/AddUserTest.php b/tests/Core/Command/Group/AddUserTest.php
new file mode 100644
index 00000000000..68c8cecdba1
--- /dev/null
+++ b/tests/Core/Command/Group/AddUserTest.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\Group;
+
+use OC\Core\Command\Group\AddUser;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class AddUserTest extends TestCase {
+ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $groupManager;
+
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $userManager;
+
+ /** @var AddUser */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->command = new AddUser($this->userManager, $this->groupManager);
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) {
+ if ($arg === 'group') {
+ return 'myGroup';
+ } elseif ($arg === 'user') {
+ return 'myUser';
+ }
+ throw new \Exception();
+ });
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testNoGroup(): void {
+ $this->groupManager->method('get')
+ ->with('myGroup')
+ ->willReturn(null);
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with('<error>group not found</error>');
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testNoUser(): void {
+ $group = $this->createMock(IGroup::class);
+ $this->groupManager->method('get')
+ ->with('myGroup')
+ ->willReturn($group);
+
+ $this->userManager->method('get')
+ ->with('myUser')
+ ->willReturn(null);
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with('<error>user not found</error>');
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testAdd(): void {
+ $group = $this->createMock(IGroup::class);
+ $this->groupManager->method('get')
+ ->with('myGroup')
+ ->willReturn($group);
+
+ $user = $this->createMock(IUser::class);
+ $this->userManager->method('get')
+ ->with('myUser')
+ ->willReturn($user);
+
+ $group->expects($this->once())
+ ->method('addUser')
+ ->with($user);
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/Group/DeleteTest.php b/tests/Core/Command/Group/DeleteTest.php
new file mode 100644
index 00000000000..289c6a7c322
--- /dev/null
+++ b/tests/Core/Command/Group/DeleteTest.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\Group;
+
+use OC\Core\Command\Group\Delete;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DeleteTest extends TestCase {
+ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $groupManager;
+
+ /** @var Delete */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->command = new Delete($this->groupManager);
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testDoesNotExists(): void {
+ $gid = 'myGroup';
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($gid) {
+ if ($arg === 'groupid') {
+ return $gid;
+ }
+ throw new \Exception();
+ });
+ $this->groupManager->method('groupExists')
+ ->with($gid)
+ ->willReturn(false);
+
+ $this->groupManager->expects($this->never())
+ ->method('get');
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with($this->equalTo('<error>Group "' . $gid . '" does not exist.</error>'));
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testDeleteAdmin(): void {
+ $gid = 'admin';
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($gid) {
+ if ($arg === 'groupid') {
+ return $gid;
+ }
+ throw new \Exception();
+ });
+
+ $this->groupManager->expects($this->never())
+ ->method($this->anything());
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with($this->equalTo('<error>Group "' . $gid . '" could not be deleted.</error>'));
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testDeleteFailed(): void {
+ $gid = 'myGroup';
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($gid) {
+ if ($arg === 'groupid') {
+ return $gid;
+ }
+ throw new \Exception();
+ });
+ $group = $this->createMock(IGroup::class);
+ $group->method('delete')
+ ->willReturn(false);
+ $this->groupManager->method('groupExists')
+ ->with($gid)
+ ->willReturn(true);
+ $this->groupManager->method('get')
+ ->with($gid)
+ ->willReturn($group);
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with($this->equalTo('<error>Group "' . $gid . '" could not be deleted. Please check the logs.</error>'));
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testDelete(): void {
+ $gid = 'myGroup';
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($gid) {
+ if ($arg === 'groupid') {
+ return $gid;
+ }
+ throw new \Exception();
+ });
+ $group = $this->createMock(IGroup::class);
+ $group->method('delete')
+ ->willReturn(true);
+ $this->groupManager->method('groupExists')
+ ->with($gid)
+ ->willReturn(true);
+ $this->groupManager->method('get')
+ ->with($gid)
+ ->willReturn($group);
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with($this->equalTo('Group "' . $gid . '" was removed'));
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/Group/InfoTest.php b/tests/Core/Command/Group/InfoTest.php
new file mode 100644
index 00000000000..87f59d2adc4
--- /dev/null
+++ b/tests/Core/Command/Group/InfoTest.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\Group;
+
+use OC\Core\Command\Group\Info;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class InfoTest extends TestCase {
+ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $groupManager;
+
+ /** @var Info|\PHPUnit\Framework\MockObject\MockObject */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->command = $this->getMockBuilder(Info::class)
+ ->setConstructorArgs([$this->groupManager])
+ ->onlyMethods(['writeArrayInOutputFormat'])
+ ->getMock();
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testDoesNotExists(): void {
+ $gid = 'myGroup';
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($gid) {
+ if ($arg === 'groupid') {
+ return $gid;
+ }
+ throw new \Exception();
+ });
+ $this->groupManager->method('get')
+ ->with($gid)
+ ->willReturn(null);
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with($this->equalTo('<error>Group "' . $gid . '" does not exist.</error>'));
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testInfo(): void {
+ $gid = 'myGroup';
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($gid) {
+ if ($arg === 'groupid') {
+ return $gid;
+ }
+ throw new \Exception();
+ });
+
+ $group = $this->createMock(IGroup::class);
+ $group->method('getGID')->willReturn($gid);
+ $group->method('getDisplayName')
+ ->willReturn('My Group');
+ $group->method('getBackendNames')
+ ->willReturn(['Database']);
+
+ $this->groupManager->method('get')
+ ->with($gid)
+ ->willReturn($group);
+
+ $this->command->expects($this->once())
+ ->method('writeArrayInOutputFormat')
+ ->with(
+ $this->equalTo($this->input),
+ $this->equalTo($this->output),
+ [
+ 'groupID' => 'myGroup',
+ 'displayName' => 'My Group',
+ 'backends' => ['Database'],
+ ]
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/Group/ListCommandTest.php b/tests/Core/Command/Group/ListCommandTest.php
new file mode 100644
index 00000000000..aaca772d714
--- /dev/null
+++ b/tests/Core/Command/Group/ListCommandTest.php
@@ -0,0 +1,204 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\Group;
+
+use OC\Core\Command\Group\ListCommand;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class ListCommandTest extends TestCase {
+ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $groupManager;
+
+ /** @var ListCommand|\PHPUnit\Framework\MockObject\MockObject */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->command = $this->getMockBuilder(ListCommand::class)
+ ->setConstructorArgs([$this->groupManager])
+ ->onlyMethods(['writeArrayInOutputFormat'])
+ ->getMock();
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testExecute(): void {
+ $group1 = $this->createMock(IGroup::class);
+ $group1->method('getGID')->willReturn('group1');
+ $group2 = $this->createMock(IGroup::class);
+ $group2->method('getGID')->willReturn('group2');
+ $group3 = $this->createMock(IGroup::class);
+ $group3->method('getGID')->willReturn('group3');
+
+ $user = $this->createMock(IUser::class);
+
+ $this->groupManager->method('search')
+ ->with(
+ '',
+ 100,
+ 42,
+ )->willReturn([$group1, $group2, $group3]);
+
+ $group1->method('getUsers')
+ ->willReturn([
+ 'user1' => $user,
+ 'user2' => $user,
+ ]);
+
+ $group2->method('getUsers')
+ ->willReturn([
+ ]);
+
+ $group3->method('getUsers')
+ ->willReturn([
+ 'user1' => $user,
+ 'user3' => $user,
+ ]);
+
+ $this->input->method('getOption')
+ ->willReturnCallback(function ($arg) {
+ if ($arg === 'limit') {
+ return '100';
+ } elseif ($arg === 'offset') {
+ return '42';
+ } elseif ($arg === 'info') {
+ return null;
+ }
+ throw new \Exception();
+ });
+
+ $this->command->expects($this->once())
+ ->method('writeArrayInOutputFormat')
+ ->with(
+ $this->equalTo($this->input),
+ $this->equalTo($this->output),
+ $this->callback(
+ fn ($iterator) => iterator_to_array($iterator) === [
+ 'group1' => [
+ 'user1',
+ 'user2',
+ ],
+ 'group2' => [
+ ],
+ 'group3' => [
+ 'user1',
+ 'user3',
+ ]
+ ]
+ )
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testInfo(): void {
+ $group1 = $this->createMock(IGroup::class);
+ $group1->method('getGID')->willReturn('group1');
+ $group1->method('getDisplayName')->willReturn('Group 1');
+ $group2 = $this->createMock(IGroup::class);
+ $group2->method('getGID')->willReturn('group2');
+ $group2->method('getDisplayName')->willReturn('Group 2');
+ $group3 = $this->createMock(IGroup::class);
+ $group3->method('getGID')->willReturn('group3');
+ $group3->method('getDisplayName')->willReturn('Group 3');
+
+ $user = $this->createMock(IUser::class);
+
+ $this->groupManager->method('search')
+ ->with(
+ '',
+ 100,
+ 42,
+ )->willReturn([$group1, $group2, $group3]);
+
+ $group1->method('getUsers')
+ ->willReturn([
+ 'user1' => $user,
+ 'user2' => $user,
+ ]);
+
+ $group1->method('getBackendNames')
+ ->willReturn(['Database']);
+
+ $group2->method('getUsers')
+ ->willReturn([
+ ]);
+
+ $group2->method('getBackendNames')
+ ->willReturn(['Database']);
+
+ $group3->method('getUsers')
+ ->willReturn([
+ 'user1' => $user,
+ 'user3' => $user,
+ ]);
+
+ $group3->method('getBackendNames')
+ ->willReturn(['LDAP']);
+
+ $this->input->method('getOption')
+ ->willReturnCallback(function ($arg) {
+ if ($arg === 'limit') {
+ return '100';
+ } elseif ($arg === 'offset') {
+ return '42';
+ } elseif ($arg === 'info') {
+ return true;
+ }
+ throw new \Exception();
+ });
+
+ $this->command->expects($this->once())
+ ->method('writeArrayInOutputFormat')
+ ->with(
+ $this->equalTo($this->input),
+ $this->equalTo($this->output),
+ $this->callback(
+ fn ($iterator) => iterator_to_array($iterator) === [
+ 'group1' => [
+ 'displayName' => 'Group 1',
+ 'backends' => ['Database'],
+ 'users' => [
+ 'user1',
+ 'user2',
+ ],
+ ],
+ 'group2' => [
+ 'displayName' => 'Group 2',
+ 'backends' => ['Database'],
+ 'users' => [],
+ ],
+ 'group3' => [
+ 'displayName' => 'Group 3',
+ 'backends' => ['LDAP'],
+ 'users' => [
+ 'user1',
+ 'user3',
+ ],
+ ]
+ ]
+ )
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/Group/RemoveUserTest.php b/tests/Core/Command/Group/RemoveUserTest.php
new file mode 100644
index 00000000000..74343e77d3f
--- /dev/null
+++ b/tests/Core/Command/Group/RemoveUserTest.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\Group;
+
+use OC\Core\Command\Group\RemoveUser;
+use OCP\IGroup;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class RemoveUserTest extends TestCase {
+ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $groupManager;
+
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $userManager;
+
+ /** @var RemoveUser */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->groupManager = $this->createMock(IGroupManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->command = new RemoveUser($this->userManager, $this->groupManager);
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) {
+ if ($arg === 'group') {
+ return 'myGroup';
+ } elseif ($arg === 'user') {
+ return 'myUser';
+ }
+ throw new \Exception();
+ });
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testNoGroup(): void {
+ $this->groupManager->method('get')
+ ->with('myGroup')
+ ->willReturn(null);
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with('<error>group not found</error>');
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testNoUser(): void {
+ $group = $this->createMock(IGroup::class);
+ $this->groupManager->method('get')
+ ->with('myGroup')
+ ->willReturn($group);
+
+ $this->userManager->method('get')
+ ->with('myUser')
+ ->willReturn(null);
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with('<error>user not found</error>');
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testAdd(): void {
+ $group = $this->createMock(IGroup::class);
+ $this->groupManager->method('get')
+ ->with('myGroup')
+ ->willReturn($group);
+
+ $user = $this->createMock(IUser::class);
+ $this->userManager->method('get')
+ ->with('myUser')
+ ->willReturn($user);
+
+ $group->expects($this->once())
+ ->method('removeUser')
+ ->with($user);
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/Log/FileTest.php b/tests/Core/Command/Log/FileTest.php
new file mode 100644
index 00000000000..1aaf398b875
--- /dev/null
+++ b/tests/Core/Command/Log/FileTest.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Log;
+
+use OC\Core\Command\Log\File;
+use OCP\IConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class FileTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $config;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $config = $this->config = $this->getMockBuilder(IConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ $this->command = new File($config);
+ }
+
+ public function testEnable(): void {
+ $this->config->method('getSystemValue')->willReturnArgument(1);
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['enable', 'true']
+ ]);
+ $this->config->expects($this->once())
+ ->method('setSystemValue')
+ ->with('log_type', 'file');
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testChangeFile(): void {
+ $this->config->method('getSystemValue')->willReturnArgument(1);
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['file', '/foo/bar/file.log']
+ ]);
+ $this->config->expects($this->once())
+ ->method('setSystemValue')
+ ->with('logfile', '/foo/bar/file.log');
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public static function changeRotateSizeProvider(): array {
+ return [
+ ['42', 42],
+ ['0', 0],
+ ['1 kB', 1024],
+ ['5MB', 5 * 1024 * 1024],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('changeRotateSizeProvider')]
+ public function testChangeRotateSize($optionValue, $configValue): void {
+ $this->config->method('getSystemValue')->willReturnArgument(1);
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['rotate-size', $optionValue]
+ ]);
+ $this->config->expects($this->once())
+ ->method('setSystemValue')
+ ->with('log_rotate_size', $configValue);
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testGetConfiguration(): void {
+ $this->config->method('getSystemValue')
+ ->willReturnMap([
+ ['log_type', 'file', 'log_type_value'],
+ ['datadirectory', \OC::$SERVERROOT . '/data', '/data/directory/'],
+ ['logfile', '/data/directory/nextcloud.log', '/var/log/nextcloud.log'],
+ ['log_rotate_size', 100 * 1024 * 1024, 5 * 1024 * 1024],
+ ]);
+
+ $calls = [
+ ['Log backend file: disabled'],
+ ['Log file: /var/log/nextcloud.log'],
+ ['Rotate at: 5 MB'],
+ ];
+ $this->consoleOutput->expects($this->exactly(3))
+ ->method('writeln')
+ ->willReturnCallback(function (string $message) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected[0], $message);
+ });
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Log/ManageTest.php b/tests/Core/Command/Log/ManageTest.php
new file mode 100644
index 00000000000..8b307048719
--- /dev/null
+++ b/tests/Core/Command/Log/ManageTest.php
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Log;
+
+use OC\Core\Command\Log\Manage;
+use OCP\IConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class ManageTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $config;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $config = $this->config = $this->getMockBuilder(IConfig::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ $this->command = new Manage($config);
+ }
+
+ public function testChangeBackend(): void {
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['backend', 'syslog']
+ ]);
+ $this->config->expects($this->once())
+ ->method('setSystemValue')
+ ->with('log_type', 'syslog');
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testChangeLevel(): void {
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['level', 'debug']
+ ]);
+ $this->config->expects($this->once())
+ ->method('setSystemValue')
+ ->with('loglevel', 0);
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testChangeTimezone(): void {
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['timezone', 'UTC']
+ ]);
+ $this->config->expects($this->once())
+ ->method('setSystemValue')
+ ->with('logtimezone', 'UTC');
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+
+ public function testValidateBackend(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ self::invokePrivate($this->command, 'validateBackend', ['notabackend']);
+ }
+
+
+ public function testValidateTimezone(): void {
+ $this->expectException(\Exception::class);
+
+ // this might need to be changed when humanity colonises Mars
+ self::invokePrivate($this->command, 'validateTimezone', ['Mars/OlympusMons']);
+ }
+
+ public static function dataConvertLevelString(): array {
+ return [
+ ['dEbug', 0],
+ ['inFO', 1],
+ ['Warning', 2],
+ ['wArn', 2],
+ ['error', 3],
+ ['eRr', 3],
+ ['fAtAl', 4],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataConvertLevelString')]
+ public function testConvertLevelString(string $levelString, int $expectedInt): void {
+ $this->assertEquals($expectedInt,
+ self::invokePrivate($this->command, 'convertLevelString', [$levelString])
+ );
+ }
+
+
+ public function testConvertLevelStringInvalid(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ self::invokePrivate($this->command, 'convertLevelString', ['abc']);
+ }
+
+ public static function dataConvertLevelNumber(): array {
+ return [
+ [0, 'Debug'],
+ [1, 'Info'],
+ [2, 'Warning'],
+ [3, 'Error'],
+ [4, 'Fatal'],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataConvertLevelNumber')]
+ public function testConvertLevelNumber(int $levelNum, string $expectedString): void {
+ $this->assertEquals($expectedString,
+ self::invokePrivate($this->command, 'convertLevelNumber', [$levelNum])
+ );
+ }
+
+
+ public function testConvertLevelNumberInvalid(): void {
+ $this->expectException(\InvalidArgumentException::class);
+
+ self::invokePrivate($this->command, 'convertLevelNumber', [11]);
+ }
+
+ public function testGetConfiguration(): void {
+ $this->config->expects($this->exactly(3))
+ ->method('getSystemValue')
+ ->willReturnMap([
+ ['log_type', 'file', 'log_type_value'],
+ ['loglevel', 2, 0],
+ ['logtimezone', 'UTC', 'logtimezone_value'],
+ ]);
+
+ $calls = [
+ ['Enabled logging backend: log_type_value'],
+ ['Log level: Debug (0)'],
+ ['Log timezone: logtimezone_value'],
+ ];
+ $this->consoleOutput->expects($this->exactly(3))
+ ->method('writeln')
+ ->willReturnCallback(function (string $message) use (&$calls): void {
+ $call = array_shift($calls);
+ $this->assertStringContainsString($call[0], $message);
+ });
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Maintenance/DataFingerprintTest.php b/tests/Core/Command/Maintenance/DataFingerprintTest.php
new file mode 100644
index 00000000000..99004a7a5f5
--- /dev/null
+++ b/tests/Core/Command/Maintenance/DataFingerprintTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Maintenance;
+
+use OC\Core\Command\Maintenance\DataFingerprint;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DataFingerprintTest extends TestCase {
+ /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
+ protected $config;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+ /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
+ protected $timeFactory;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->config = $this->getMockBuilder(IConfig::class)->getMock();
+ $this->timeFactory = $this->getMockBuilder(ITimeFactory::class)->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var IConfig $config */
+ $this->command = new DataFingerprint($this->config, $this->timeFactory);
+ }
+
+ public function testSetFingerPrint(): void {
+ $this->timeFactory->expects($this->once())
+ ->method('getTime')
+ ->willReturn(42);
+ $this->config->expects($this->once())
+ ->method('setSystemValue')
+ ->with('data-fingerprint', md5(42));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Maintenance/Mimetype/UpdateDBTest.php b/tests/Core/Command/Maintenance/Mimetype/UpdateDBTest.php
new file mode 100644
index 00000000000..b85dcf87bbc
--- /dev/null
+++ b/tests/Core/Command/Maintenance/Mimetype/UpdateDBTest.php
@@ -0,0 +1,174 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\Maintenance\Mimetype;
+
+use OC\Core\Command\Maintenance\Mimetype\UpdateDB;
+use OC\Files\Type\Detection;
+use OC\Files\Type\Loader;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\IMimeTypeLoader;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class UpdateDBTest extends TestCase {
+ /** @var IMimeTypeDetector */
+ protected $detector;
+ /** @var IMimeTypeLoader */
+ protected $loader;
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->detector = $this->createMock(Detection::class);
+ $this->loader = $this->createMock(Loader::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+
+ $this->command = new UpdateDB($this->detector, $this->loader);
+ }
+
+ public function testNoop(): void {
+ $this->consoleInput->method('getOption')
+ ->with('repair-filecache')
+ ->willReturn(false);
+
+ $this->detector->expects($this->once())
+ ->method('getAllMappings')
+ ->willReturn([
+ 'ext' => ['testing/existingmimetype']
+ ]);
+ $this->loader->expects($this->once())
+ ->method('exists')
+ ->with('testing/existingmimetype')
+ ->willReturn(true);
+
+ $this->loader->expects($this->never())
+ ->method('updateFilecache');
+
+ $calls = [
+ 'Added 0 new mimetypes',
+ 'Updated 0 filecache rows',
+ ];
+ $this->consoleOutput->expects($this->exactly(2))
+ ->method('writeln')
+ ->willReturnCallback(function ($message) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertStringContainsString($expected, $message);
+ });
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testAddMimetype(): void {
+ $this->consoleInput->method('getOption')
+ ->with('repair-filecache')
+ ->willReturn(false);
+
+ $this->detector->expects($this->once())
+ ->method('getAllMappings')
+ ->willReturn([
+ 'ext' => ['testing/existingmimetype'],
+ 'new' => ['testing/newmimetype']
+ ]);
+ $this->loader->expects($this->exactly(2))
+ ->method('exists')
+ ->willReturnMap([
+ ['testing/existingmimetype', true],
+ ['testing/newmimetype', false],
+ ]);
+ $this->loader->expects($this->exactly(2))
+ ->method('getId')
+ ->willReturnMap([
+ ['testing/existingmimetype', 1],
+ ['testing/newmimetype', 2],
+ ]);
+
+ $this->loader->expects($this->once())
+ ->method('updateFilecache')
+ ->with('new', 2)
+ ->willReturn(3);
+
+ $calls = [
+ 'Added mimetype "testing/newmimetype" to database',
+ 'Updated 3 filecache rows for mimetype "testing/newmimetype"',
+ 'Added 1 new mimetypes',
+ 'Updated 3 filecache rows',
+ ];
+ $this->consoleOutput->expects($this->exactly(4))
+ ->method('writeln')
+ ->willReturnCallback(function ($message) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertStringContainsString($expected, $message);
+ });
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testSkipComments(): void {
+ $this->detector->expects($this->once())
+ ->method('getAllMappings')
+ ->willReturn([
+ '_comment' => 'some comment in the JSON'
+ ]);
+ $this->loader->expects($this->never())
+ ->method('exists');
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testRepairFilecache(): void {
+ $this->consoleInput->method('getOption')
+ ->with('repair-filecache')
+ ->willReturn(true);
+
+ $this->detector->expects($this->once())
+ ->method('getAllMappings')
+ ->willReturn([
+ 'ext' => ['testing/existingmimetype'],
+ ]);
+ $this->loader->expects($this->exactly(1))
+ ->method('exists')
+ ->willReturnMap([
+ ['testing/existingmimetype', true],
+ ]);
+ $this->loader->expects($this->exactly(1))
+ ->method('getId')
+ ->willReturnMap([
+ ['testing/existingmimetype', 1],
+ ]);
+
+ $this->loader->expects($this->once())
+ ->method('updateFilecache')
+ ->with('ext', 1)
+ ->willReturn(3);
+
+ $calls = [
+ 'Updated 3 filecache rows for mimetype "testing/existingmimetype"',
+ 'Added 0 new mimetypes',
+ 'Updated 3 filecache rows',
+ ];
+ $this->consoleOutput->expects($this->exactly(3))
+ ->method('writeln')
+ ->willReturnCallback(function ($message) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertStringContainsString($expected, $message);
+ });
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Maintenance/ModeTest.php b/tests/Core/Command/Maintenance/ModeTest.php
new file mode 100644
index 00000000000..5a9a90b0197
--- /dev/null
+++ b/tests/Core/Command/Maintenance/ModeTest.php
@@ -0,0 +1,151 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace Tests\Core\Command\Maintenance;
+
+use OC\Core\Command\Maintenance\Mode;
+use OCP\IConfig;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+/**
+ * This class provides tests methods for the Mode command.
+ *
+ * @package Tests\Core\Command\Maintenance
+ */
+class ModeTest extends TestCase {
+ /**
+ * A config mock passed to the command.
+ *
+ * @var IConfig|MockObject
+ */
+ private $config;
+
+ /**
+ * Holds a Mode command instance with a config mock.
+ *
+ * @var Mode
+ */
+ private $mode;
+
+ /**
+ * An input mock for tests.
+ *
+ * @var InputInterface|MockObject
+ */
+ private $input;
+
+ /**
+ * An output mock for tests.
+ *
+ * @var OutputInterface|MockObject
+ */
+ private $output;
+
+ /**
+ * Setups the test environment.
+ *
+ * @return void
+ */
+ protected function setUp(): void {
+ parent::setUp();
+ $this->config = $this->getMockBuilder(IConfig::class)
+ ->getMock();
+ $this->mode = new Mode($this->config);
+ $this->input = $this->getMockBuilder(InputInterface::class)
+ ->getMock();
+ $this->output = $this->getMockBuilder(OutputInterface::class)
+ ->getMock();
+ }
+
+ /**
+ * Provides test data for the execute test.
+ *
+ * @return array
+ */
+ public static function getExecuteTestData(): array {
+ return [
+ 'off -> on' => [
+ 'on', // command option
+ false, // current maintenance mode state
+ true, // expected maintenance mode state, null for no change
+ 'Maintenance mode enabled', // expected output
+ ],
+ 'on -> off' => [
+ 'off',
+ true,
+ false,
+ 'Maintenance mode disabled',
+ ],
+ 'on -> on' => [
+ 'on',
+ true,
+ null,
+ 'Maintenance mode already enabled',
+ ],
+ 'off -> off' => [
+ 'off',
+ false,
+ null,
+ 'Maintenance mode already disabled',
+ ],
+ 'no option, maintenance enabled' => [
+ '',
+ true,
+ null,
+ 'Maintenance mode is currently enabled',
+ ],
+ 'no option, maintenance disabled' => [
+ '',
+ false,
+ null,
+ 'Maintenance mode is currently disabled',
+ ],
+ ];
+ }
+
+ /**
+ * Asserts that execute works as expected.
+ *
+ * @param string $option The command option.
+ * @param bool $currentMaintenanceState The current maintenance state.
+ * @param null|bool $expectedMaintenanceState
+ * The expected maintenance state. Null for no change.
+ * @param string $expectedOutput The expected command output.
+ * @throws \Exception
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('getExecuteTestData')]
+ public function testExecute(
+ string $option,
+ bool $currentMaintenanceState,
+ $expectedMaintenanceState,
+ string $expectedOutput,
+ ): void {
+ $this->config->expects($this->any())
+ ->method('getSystemValueBool')
+ ->willReturn($currentMaintenanceState);
+
+ if ($expectedMaintenanceState !== null) {
+ $this->config->expects($this->once())
+ ->method('setSystemValue')
+ ->with('maintenance', $expectedMaintenanceState);
+ }
+
+ $this->input->expects($this->any())
+ ->method('getOption')
+ ->willReturnCallback(function ($callOption) use ($option) {
+ return $callOption === $option;
+ });
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with($expectedOutput);
+
+ $this->mode->run($this->input, $this->output);
+ }
+}
diff --git a/tests/Core/Command/Maintenance/UpdateTheme.php b/tests/Core/Command/Maintenance/UpdateTheme.php
new file mode 100644
index 00000000000..9c9a2b903a7
--- /dev/null
+++ b/tests/Core/Command/Maintenance/UpdateTheme.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Command\Maintenance;
+
+use OC\Core\Command\Maintenance\UpdateTheme;
+use OC\Files\Type\Detection;
+use OCP\Files\IMimeTypeDetector;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class UpdateThemeTest extends TestCase {
+ /** @var IMimeTypeDetector */
+ protected $detector;
+ /** @var ICacheFactory */
+ protected $cacheFactory;
+
+
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->detector = $this->createMock(Detection::class);
+ $this->cacheFactory = $this->createMock(ICacheFactory::class);
+
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ $this->command = new UpdateTheme($this->detector, $this->cacheFactory);
+ }
+
+ public function testThemeUpdate(): void {
+ $this->consoleInput->method('getOption')
+ ->with('maintenance:theme:update')
+ ->willReturn(true);
+ $this->detector->expects($this->once())
+ ->method('getAllAliases')
+ ->willReturn([]);
+ $cache = $this->createMock(ICache::class);
+ $cache->expects($this->once())
+ ->method('clear')
+ ->with('');
+ $this->cacheFactory->expects($this->once())
+ ->method('createDistributed')
+ ->with('imagePath')
+ ->willReturn($cache);
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/Preview/CleanupTest.php b/tests/Core/Command/Preview/CleanupTest.php
new file mode 100644
index 00000000000..e4a83246e5b
--- /dev/null
+++ b/tests/Core/Command/Preview/CleanupTest.php
@@ -0,0 +1,175 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace Core\Command\Preview;
+
+use OC\Core\Command\Preview\Cleanup;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class CleanupTest extends TestCase {
+ private IRootFolder&MockObject $rootFolder;
+ private LoggerInterface&MockObject $logger;
+ private InputInterface&MockObject $input;
+ private OutputInterface&MockObject $output;
+ private Cleanup $repair;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->repair = new Cleanup(
+ $this->rootFolder,
+ $this->logger,
+ );
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testCleanup(): void {
+ $previewFolder = $this->createMock(Folder::class);
+ $previewFolder->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ $previewFolder->expects($this->once())
+ ->method('delete');
+
+ $appDataFolder = $this->createMock(Folder::class);
+ $appDataFolder->expects($this->once())->method('get')->with('preview')->willReturn($previewFolder);
+ $appDataFolder->expects($this->once())->method('newFolder')->with('preview');
+
+ $this->rootFolder->expects($this->once())
+ ->method('getAppDataDirectoryName')
+ ->willReturn('appdata_some_id');
+
+ $this->rootFolder->expects($this->once())
+ ->method('get')
+ ->with('appdata_some_id')
+ ->willReturn($appDataFolder);
+
+ $this->output->expects($this->exactly(3))->method('writeln')
+ ->with(self::callback(function (string $message): bool {
+ static $i = 0;
+ return match (++$i) {
+ 1 => $message === 'Preview folder deleted',
+ 2 => $message === 'Preview folder recreated',
+ 3 => $message === 'Previews removed'
+ };
+ }));
+
+ $this->assertEquals(0, $this->repair->run($this->input, $this->output));
+ }
+
+ public function testCleanupWhenNotDeletable(): void {
+ $previewFolder = $this->createMock(Folder::class);
+ $previewFolder->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(false);
+
+ $previewFolder->expects($this->never())
+ ->method('delete');
+
+ $appDataFolder = $this->createMock(Folder::class);
+ $appDataFolder->expects($this->once())->method('get')->with('preview')->willReturn($previewFolder);
+ $appDataFolder->expects($this->never())->method('newFolder')->with('preview');
+
+ $this->rootFolder->expects($this->once())
+ ->method('getAppDataDirectoryName')
+ ->willReturn('appdata_some_id');
+
+ $this->rootFolder->expects($this->once())
+ ->method('get')
+ ->with('appdata_some_id')
+ ->willReturn($appDataFolder);
+
+ $this->logger->expects($this->once())->method('error')->with("Previews can't be removed: preview folder isn't deletable");
+ $this->output->expects($this->once())->method('writeln')->with("Previews can't be removed: preview folder isn't deletable");
+
+ $this->assertEquals(1, $this->repair->run($this->input, $this->output));
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataForTestCleanupWithDeleteException')]
+ public function testCleanupWithDeleteException(string $exceptionClass, string $errorMessage): void {
+ $previewFolder = $this->createMock(Folder::class);
+ $previewFolder->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ $previewFolder->expects($this->once())
+ ->method('delete')
+ ->willThrowException(new $exceptionClass());
+
+ $appDataFolder = $this->createMock(Folder::class);
+ $appDataFolder->expects($this->once())->method('get')->with('preview')->willReturn($previewFolder);
+ $appDataFolder->expects($this->never())->method('newFolder')->with('preview');
+
+ $this->rootFolder->expects($this->once())
+ ->method('getAppDataDirectoryName')
+ ->willReturn('appdata_some_id');
+
+ $this->rootFolder->expects($this->once())
+ ->method('get')
+ ->with('appdata_some_id')
+ ->willReturn($appDataFolder);
+
+ $this->logger->expects($this->once())->method('error')->with($errorMessage);
+ $this->output->expects($this->once())->method('writeln')->with($errorMessage);
+
+ $this->assertEquals(1, $this->repair->run($this->input, $this->output));
+ }
+
+ public static function dataForTestCleanupWithDeleteException(): array {
+ return [
+ [NotFoundException::class, "Previews weren't deleted: preview folder was not found while deleting it"],
+ [NotPermittedException::class, "Previews weren't deleted: you don't have the permission to delete preview folder"],
+ ];
+ }
+
+ public function testCleanupWithCreateException(): void {
+ $previewFolder = $this->createMock(Folder::class);
+ $previewFolder->expects($this->once())
+ ->method('isDeletable')
+ ->willReturn(true);
+
+ $previewFolder->expects($this->once())
+ ->method('delete');
+
+ $appDataFolder = $this->createMock(Folder::class);
+ $appDataFolder->expects($this->once())->method('get')->with('preview')->willReturn($previewFolder);
+ $appDataFolder->expects($this->once())->method('newFolder')->with('preview')->willThrowException(new NotPermittedException());
+
+ $this->rootFolder->expects($this->once())
+ ->method('getAppDataDirectoryName')
+ ->willReturn('appdata_some_id');
+
+ $this->rootFolder->expects($this->once())
+ ->method('get')
+ ->with('appdata_some_id')
+ ->willReturn($appDataFolder);
+
+ $this->output->expects($this->exactly(2))->method('writeln')
+ ->with(self::callback(function (string $message): bool {
+ static $i = 0;
+ return match (++$i) {
+ 1 => $message === 'Preview folder deleted',
+ 2 => $message === "Preview folder was deleted, but you don't have the permission to create preview folder",
+ };
+ }));
+
+ $this->logger->expects($this->once())->method('error')->with("Preview folder was deleted, but you don't have the permission to create preview folder");
+
+ $this->assertEquals(1, $this->repair->run($this->input, $this->output));
+ }
+}
diff --git a/tests/Core/Command/Preview/RepairTest.php b/tests/Core/Command/Preview/RepairTest.php
new file mode 100644
index 00000000000..9b9cde6dd95
--- /dev/null
+++ b/tests/Core/Command/Preview/RepairTest.php
@@ -0,0 +1,153 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace Tests\Core\Command\Preview;
+
+use bantu\IniGetWrapper\IniGetWrapper;
+use OC\Core\Command\Preview\Repair;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\IConfig;
+use OCP\Lock\ILockingProvider;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Formatter\OutputFormatterInterface;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\ConsoleOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class RepairTest extends TestCase {
+ /** @var IConfig|MockObject */
+ private $config;
+ /** @var IRootFolder|MockObject */
+ private $rootFolder;
+ /** @var LoggerInterface|MockObject */
+ private $logger;
+ /** @var IniGetWrapper|MockObject */
+ private $iniGetWrapper;
+ /** @var InputInterface|MockObject */
+ private $input;
+ /** @var OutputInterface|MockObject */
+ private $output;
+ /** @var string */
+ private $outputLines = '';
+ /** @var Repair */
+ private $repair;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->config = $this->getMockBuilder(IConfig::class)
+ ->getMock();
+ $this->rootFolder = $this->getMockBuilder(IRootFolder::class)
+ ->getMock();
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)
+ ->getMock();
+ $this->iniGetWrapper = $this->getMockBuilder(IniGetWrapper::class)
+ ->getMock();
+ $this->repair = new Repair(
+ $this->config,
+ $this->rootFolder,
+ $this->logger,
+ $this->iniGetWrapper,
+ $this->createMock(ILockingProvider::class)
+ );
+ $this->input = $this->createMock(InputInterface::class);
+ $this->input->expects($this->any())
+ ->method('getOption')
+ ->willReturnCallback(function ($parameter) {
+ if ($parameter === 'batch') {
+ return true;
+ }
+ return null;
+ });
+ $this->output = $this->getMockBuilder(ConsoleOutput::class)
+ ->onlyMethods(['section', 'writeln', 'getFormatter'])
+ ->getMock();
+ $self = $this;
+
+ /* We need format method to return a string */
+ $outputFormatter = $this->createMock(OutputFormatterInterface::class);
+ $outputFormatter->method('isDecorated')->willReturn(false);
+ $outputFormatter->method('format')->willReturnArgument(0);
+
+ $this->output->expects($this->any())
+ ->method('getFormatter')
+ ->willReturn($outputFormatter);
+ $this->output->expects($this->any())
+ ->method('writeln')
+ ->willReturnCallback(function ($line) use ($self): void {
+ $self->outputLines .= $line . "\n";
+ });
+ }
+
+ public static function dataEmptyTest(): array {
+ /** directoryNames, expectedOutput */
+ return [
+ [
+ [],
+ 'All previews are already migrated.'
+ ],
+ [
+ [['name' => 'a'], ['name' => 'b'], ['name' => 'c']],
+ 'All previews are already migrated.'
+ ],
+ [
+ [['name' => '0', 'content' => ['folder', 'folder']], ['name' => 'b'], ['name' => 'c']],
+ 'All previews are already migrated.'
+ ],
+ [
+ [['name' => '0', 'content' => ['file', 'folder', 'folder']], ['name' => 'b'], ['name' => 'c']],
+ 'A total of 1 preview files need to be migrated.'
+ ],
+ [
+ [['name' => '23'], ['name' => 'b'], ['name' => 'c']],
+ 'A total of 1 preview files need to be migrated.'
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataEmptyTest')]
+ public function testEmptyExecute($directoryNames, $expectedOutput): void {
+ $previewFolder = $this->getMockBuilder(Folder::class)
+ ->getMock();
+ $directories = array_map(function ($element) {
+ $dir = $this->getMockBuilder(Folder::class)
+ ->getMock();
+ $dir->expects($this->any())
+ ->method('getName')
+ ->willReturn($element['name']);
+ if (isset($element['content'])) {
+ $list = [];
+ foreach ($element['content'] as $item) {
+ if ($item === 'file') {
+ $list[] = $this->getMockBuilder(Node::class)
+ ->getMock();
+ } elseif ($item === 'folder') {
+ $list[] = $this->getMockBuilder(Folder::class)
+ ->getMock();
+ }
+ }
+ $dir->expects($this->once())
+ ->method('getDirectoryListing')
+ ->willReturn($list);
+ }
+ return $dir;
+ }, $directoryNames);
+ $previewFolder->expects($this->once())
+ ->method('getDirectoryListing')
+ ->willReturn($directories);
+ $this->rootFolder->expects($this->once())
+ ->method('get')
+ ->with('appdata_/preview')
+ ->willReturn($previewFolder);
+
+ $this->repair->run($this->input, $this->output);
+
+ $this->assertStringContainsString($expectedOutput, $this->outputLines);
+ }
+}
diff --git a/tests/Core/Command/SystemTag/AddTest.php b/tests/Core/Command/SystemTag/AddTest.php
new file mode 100644
index 00000000000..5f3c7174758
--- /dev/null
+++ b/tests/Core/Command/SystemTag/AddTest.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\SystemTag;
+
+use OC\Core\Command\SystemTag\Add;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\TagAlreadyExistsException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class AddTest extends TestCase {
+ /** @var ISystemTagManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $systemTagManager;
+
+ /** @var ListCommand|\PHPUnit\Framework\MockObject\MockObject */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->systemTagManager = $this->createMock(ISystemTagManager::class);
+ $this->command = $this->getMockBuilder(Add::class)
+ ->setConstructorArgs([$this->systemTagManager])
+ ->onlyMethods(['writeArrayInOutputFormat'])
+ ->getMock();
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testExecute(): void {
+ $tagId = '42';
+ $tagName = 'wichtig';
+ $tagAccess = 'public';
+
+ $tag = $this->createMock(ISystemTag::class);
+ $tag->method('getId')->willReturn($tagId);
+ $tag->method('getName')->willReturn($tagName);
+ $tag->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_PUBLIC);
+
+ $this->systemTagManager->method('createTag')
+ ->with(
+ $tagName,
+ true,
+ true
+ )->willReturn($tag);
+
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($tagName, $tagAccess) {
+ if ($arg === 'name') {
+ return $tagName;
+ } elseif ($arg === 'access') {
+ return $tagAccess;
+ }
+ throw new \Exception();
+ });
+
+ $this->command->expects($this->once())
+ ->method('writeArrayInOutputFormat')
+ ->with(
+ $this->equalTo($this->input),
+ $this->equalTo($this->output),
+ [
+ 'id' => $tagId,
+ 'name' => $tagName,
+ 'access' => $tagAccess,
+ ]
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testAlreadyExists(): void {
+ $tagId = '42';
+ $tagName = 'wichtig';
+ $tagAccess = 'public';
+
+ $tag = $this->createMock(ISystemTag::class);
+ $tag->method('getId')->willReturn($tagId);
+ $tag->method('getName')->willReturn($tagName);
+ $tag->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_PUBLIC);
+
+ $this->systemTagManager->method('createTag')
+ ->willReturnCallback(function ($tagName, $userVisible, $userAssignable): void {
+ throw new TagAlreadyExistsException(
+ 'Tag ("' . $tagName . '", ' . $userVisible . ', ' . $userAssignable . ') already exists'
+ );
+ });
+
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($tagName, $tagAccess) {
+ if ($arg === 'name') {
+ return $tagName;
+ } elseif ($arg === 'access') {
+ return $tagAccess;
+ }
+ throw new \Exception();
+ });
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with(
+ '<error>Tag ("wichtig", 1, 1) already exists</error>'
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/SystemTag/DeleteTest.php b/tests/Core/Command/SystemTag/DeleteTest.php
new file mode 100644
index 00000000000..bf756311000
--- /dev/null
+++ b/tests/Core/Command/SystemTag/DeleteTest.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\SystemTag;
+
+use OC\Core\Command\SystemTag\Delete;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\TagNotFoundException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DeleteTest extends TestCase {
+ /** @var ISystemTagManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $systemTagManager;
+
+ /** @var ListCommand|\PHPUnit\Framework\MockObject\MockObject */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->systemTagManager = $this->createMock(ISystemTagManager::class);
+ $this->command = $this->getMockBuilder(Delete::class)
+ ->setConstructorArgs([$this->systemTagManager])
+ ->onlyMethods(['writeArrayInOutputFormat'])
+ ->getMock();
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testExecute(): void {
+ $tagId = 69;
+
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($tagId) {
+ if ($arg === 'id') {
+ return $tagId;
+ }
+ throw new \Exception();
+ });
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with('<info>The specified tag was deleted</info>');
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testNotFound(): void {
+ $tagId = 69;
+
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($tagId) {
+ if ($arg === 'id') {
+ return $tagId;
+ }
+ throw new \Exception();
+ });
+
+ $this->systemTagManager->method('deleteTags')
+ ->willReturnCallback(function ($tagId): void {
+ throw new TagNotFoundException();
+ });
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with('<error>Tag not found</error>');
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/SystemTag/EditTest.php b/tests/Core/Command/SystemTag/EditTest.php
new file mode 100644
index 00000000000..b22151e3608
--- /dev/null
+++ b/tests/Core/Command/SystemTag/EditTest.php
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\SystemTag;
+
+use OC\Core\Command\SystemTag\Edit;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use OCP\SystemTag\TagAlreadyExistsException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class EditTest extends TestCase {
+ /** @var ISystemTagManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $systemTagManager;
+
+ /** @var ListCommand|\PHPUnit\Framework\MockObject\MockObject */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->systemTagManager = $this->createMock(ISystemTagManager::class);
+ $this->command = $this->getMockBuilder(Edit::class)
+ ->setConstructorArgs([$this->systemTagManager])
+ ->onlyMethods(['writeArrayInOutputFormat'])
+ ->getMock();
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testExecute(): void {
+ $tagId = '5';
+ $tagName = 'unwichtige Dateien';
+ $newTagName = 'moderat wichtige Dateien';
+ $newTagAccess = 'restricted';
+ $newTagUserVisible = true;
+ $newTagUserAssignable = false;
+
+ $tag = $this->createMock(ISystemTag::class);
+ $tag->method('getId')->willReturn($tagId);
+ $tag->method('getName')->willReturn($tagName);
+ $tag->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_INVISIBLE);
+
+ $this->systemTagManager->method('getTagsByIds')
+ ->with($tagId)
+ ->willReturn([$tag]);
+
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($tagId) {
+ if ($arg === 'id') {
+ return $tagId;
+ }
+ throw new \Exception();
+ });
+
+ $this->input->method('getOption')
+ ->willReturnCallback(function ($arg) use ($newTagName, $newTagAccess) {
+ if ($arg === 'name') {
+ return $newTagName;
+ } elseif ($arg === 'access') {
+ return $newTagAccess;
+ }
+ throw new \Exception();
+ });
+
+ $this->systemTagManager->expects($this->once())
+ ->method('updateTag')
+ ->with(
+ $tagId,
+ $newTagName,
+ $newTagUserVisible,
+ $newTagUserAssignable,
+ ''
+ );
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with(
+ '<info>Tag updated ("' . $newTagName . '", ' . json_encode($newTagUserVisible) . ', ' . json_encode($newTagUserAssignable) . ', "")</info>'
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testAlreadyExists(): void {
+ $tagId = '5';
+ $tagName = 'unwichtige Dateien';
+ $tagUserVisible = false;
+ $tagUserAssignable = false;
+ $newTagName = 'moderat wichtige Dateien';
+ $newTagAccess = 'restricted';
+ $newTagUserVisible = true;
+ $newTagUserAssignable = false;
+
+ $tag = $this->createMock(ISystemTag::class);
+ $tag->method('getId')->willReturn($tagId);
+ $tag->method('getName')->willReturn($tagName);
+ $tag->method('isUserVisible')->willReturn($tagUserVisible);
+ $tag->method('isUserAssignable')->willReturn($tagUserAssignable);
+ $tag->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_INVISIBLE);
+
+ $this->systemTagManager->method('getTagsByIds')
+ ->with($tagId)
+ ->willReturn([$tag]);
+
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($tagId) {
+ if ($arg === 'id') {
+ return $tagId;
+ }
+ throw new \Exception();
+ });
+
+ $this->input->method('getOption')
+ ->willReturnCallback(function ($arg) use ($newTagName, $newTagAccess) {
+ if ($arg === 'name') {
+ return $newTagName;
+ } elseif ($arg === 'access') {
+ return $newTagAccess;
+ }
+ throw new \Exception();
+ });
+
+ $this->systemTagManager->method('updateTag')
+ ->willReturnCallback(function ($tagId, $tagName, $userVisible, $userAssignable): void {
+ throw new TagAlreadyExistsException(
+ 'Tag ("' . $tagName . '", ' . $userVisible . ', ' . $userAssignable . ') already exists'
+ );
+ });
+
+ $this->systemTagManager->expects($this->once())
+ ->method('updateTag')
+ ->with(
+ $tagId,
+ $newTagName,
+ $newTagUserVisible,
+ $newTagUserAssignable,
+ ''
+ );
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with(
+ '<error>Tag ("' . $newTagName . '", ' . $newTagUserVisible . ', ' . $newTagUserAssignable . ') already exists</error>'
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+
+ public function testNotFound(): void {
+ $tagId = '404';
+
+ $this->input->method('getArgument')
+ ->willReturnCallback(function ($arg) use ($tagId) {
+ if ($arg === 'id') {
+ return $tagId;
+ }
+ throw new \Exception();
+ });
+
+ $this->systemTagManager->method('getTagsByIds')
+ ->with($tagId)
+ ->willReturn([]);
+
+ $this->output->expects($this->once())
+ ->method('writeln')
+ ->with(
+ '<error>Tag not found</error>'
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/SystemTag/ListCommandTest.php b/tests/Core/Command/SystemTag/ListCommandTest.php
new file mode 100644
index 00000000000..e1ff8290633
--- /dev/null
+++ b/tests/Core/Command/SystemTag/ListCommandTest.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\SystemTag;
+
+use OC\Core\Command\SystemTag\ListCommand;
+use OCP\SystemTag\ISystemTag;
+use OCP\SystemTag\ISystemTagManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class ListCommandTest extends TestCase {
+ /** @var ISystemTagManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $systemTagManager;
+
+ /** @var ListCommand|\PHPUnit\Framework\MockObject\MockObject */
+ private $command;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $input;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $output;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->systemTagManager = $this->createMock(ISystemTagManager::class);
+ $this->command = $this->getMockBuilder(ListCommand::class)
+ ->setConstructorArgs([$this->systemTagManager])
+ ->onlyMethods(['writeArrayInOutputFormat'])
+ ->getMock();
+
+ $this->input = $this->createMock(InputInterface::class);
+ $this->output = $this->createMock(OutputInterface::class);
+ }
+
+ public function testExecute(): void {
+ $tag1 = $this->createMock(ISystemTag::class);
+ $tag1->method('getId')->willReturn('1');
+ $tag1->method('getName')->willReturn('public_tag');
+ $tag1->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_PUBLIC);
+ $tag2 = $this->createMock(ISystemTag::class);
+ $tag2->method('getId')->willReturn('2');
+ $tag2->method('getName')->willReturn('restricted_tag');
+ $tag2->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_RESTRICTED);
+ $tag3 = $this->createMock(ISystemTag::class);
+ $tag3->method('getId')->willReturn('3');
+ $tag3->method('getName')->willReturn('invisible_tag');
+ $tag3->method('getAccessLevel')->willReturn(ISystemTag::ACCESS_LEVEL_INVISIBLE);
+
+ $this->systemTagManager->method('getAllTags')
+ ->with(
+ null,
+ null
+ )->willReturn([$tag1, $tag2, $tag3]);
+
+ $this->input->method('getOption')
+ ->willReturnCallback(function ($arg) {
+ if ($arg === 'visibilityFilter') {
+ return null;
+ } elseif ($arg === 'nameSearchPattern') {
+ return null;
+ }
+ throw new \Exception();
+ });
+
+ $this->command->expects($this->once())
+ ->method('writeArrayInOutputFormat')
+ ->with(
+ $this->equalTo($this->input),
+ $this->equalTo($this->output),
+ [
+ '1' => [
+ 'name' => 'public_tag',
+ 'access' => 'public',
+ ],
+ '2' => [
+ 'name' => 'restricted_tag',
+ 'access' => 'restricted',
+ ],
+ '3' => [
+ 'name' => 'invisible_tag',
+ 'access' => 'invisible',
+ ]
+ ]
+ );
+
+ $this->invokePrivate($this->command, 'execute', [$this->input, $this->output]);
+ }
+}
diff --git a/tests/Core/Command/TwoFactorAuth/CleanupTest.php b/tests/Core/Command/TwoFactorAuth/CleanupTest.php
new file mode 100644
index 00000000000..1d4731ff0c2
--- /dev/null
+++ b/tests/Core/Command/TwoFactorAuth/CleanupTest.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Core\Command\TwoFactorAuth;
+
+use OC\Core\Command\TwoFactorAuth\Cleanup;
+use OCP\Authentication\TwoFactorAuth\IRegistry;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+class CleanupTest extends TestCase {
+ /** @var IRegistry|MockObject */
+ private $registry;
+
+ /** @var IUserManager|MockObject */
+ private $userManager;
+
+ /** @var CommandTester */
+ private $cmd;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->registry = $this->createMock(IRegistry::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+
+ $cmd = new Cleanup($this->registry, $this->userManager);
+ $this->cmd = new CommandTester($cmd);
+ }
+
+ public function testCleanup(): void {
+ $this->registry->expects($this->once())
+ ->method('cleanUp')
+ ->with('u2f');
+
+ $rc = $this->cmd->execute([
+ 'provider-id' => 'u2f',
+ ]);
+
+ $this->assertEquals(0, $rc);
+ $output = $this->cmd->getDisplay();
+ $this->assertStringContainsString('All user-provider associations for provider u2f have been removed', $output);
+ }
+}
diff --git a/tests/Core/Command/TwoFactorAuth/DisableTest.php b/tests/Core/Command/TwoFactorAuth/DisableTest.php
new file mode 100644
index 00000000000..ab6b10f8964
--- /dev/null
+++ b/tests/Core/Command/TwoFactorAuth/DisableTest.php
@@ -0,0 +1,94 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\TwoFactorAuth;
+
+use OC\Authentication\TwoFactorAuth\ProviderManager;
+use OC\Core\Command\TwoFactorAuth\Disable;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+class DisableTest extends TestCase {
+ /** @var ProviderManager|MockObject */
+ private $providerManager;
+
+ /** @var IUserManager|MockObject */
+ private $userManager;
+
+ /** @var CommandTester */
+ private $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->providerManager = $this->createMock(ProviderManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+
+ $cmd = new Disable($this->providerManager, $this->userManager);
+ $this->command = new CommandTester($cmd);
+ }
+
+ public function testInvalidUID(): void {
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('nope')
+ ->willReturn(null);
+
+ $rc = $this->command->execute([
+ 'uid' => 'nope',
+ 'provider_id' => 'nope',
+ ]);
+
+ $this->assertEquals(1, $rc);
+ $this->assertStringContainsString('Invalid UID', $this->command->getDisplay());
+ }
+
+ public function testEnableNotSupported(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('ricky')
+ ->willReturn($user);
+ $this->providerManager->expects($this->once())
+ ->method('tryDisableProviderFor')
+ ->with('totp', $user)
+ ->willReturn(false);
+
+ $rc = $this->command->execute([
+ 'uid' => 'ricky',
+ 'provider_id' => 'totp',
+ ]);
+
+ $this->assertEquals(2, $rc);
+ $this->assertStringContainsString('The provider does not support this operation', $this->command->getDisplay());
+ }
+
+ public function testEnabled(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('ricky')
+ ->willReturn($user);
+ $this->providerManager->expects($this->once())
+ ->method('tryDisableProviderFor')
+ ->with('totp', $user)
+ ->willReturn(true);
+
+ $rc = $this->command->execute([
+ 'uid' => 'ricky',
+ 'provider_id' => 'totp',
+ ]);
+
+ $this->assertEquals(0, $rc);
+ $this->assertStringContainsString('Two-factor provider totp disabled for user ricky', $this->command->getDisplay());
+ }
+}
diff --git a/tests/Core/Command/TwoFactorAuth/EnableTest.php b/tests/Core/Command/TwoFactorAuth/EnableTest.php
new file mode 100644
index 00000000000..7c34d6692c5
--- /dev/null
+++ b/tests/Core/Command/TwoFactorAuth/EnableTest.php
@@ -0,0 +1,94 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Command\TwoFactorAuth;
+
+use OC\Authentication\TwoFactorAuth\ProviderManager;
+use OC\Core\Command\TwoFactorAuth\Enable;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+class EnableTest extends TestCase {
+ /** @var ProviderManager|MockObject */
+ private $providerManager;
+
+ /** @var IUserManager|MockObject */
+ private $userManager;
+
+ /** @var CommandTester */
+ private $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->providerManager = $this->createMock(ProviderManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+
+ $cmd = new Enable($this->providerManager, $this->userManager);
+ $this->command = new CommandTester($cmd);
+ }
+
+ public function testInvalidUID(): void {
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('nope')
+ ->willReturn(null);
+
+ $rc = $this->command->execute([
+ 'uid' => 'nope',
+ 'provider_id' => 'nope',
+ ]);
+
+ $this->assertEquals(1, $rc);
+ $this->assertStringContainsString('Invalid UID', $this->command->getDisplay());
+ }
+
+ public function testEnableNotSupported(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('belle')
+ ->willReturn($user);
+ $this->providerManager->expects($this->once())
+ ->method('tryEnableProviderFor')
+ ->with('totp', $user)
+ ->willReturn(false);
+
+ $rc = $this->command->execute([
+ 'uid' => 'belle',
+ 'provider_id' => 'totp',
+ ]);
+
+ $this->assertEquals(2, $rc);
+ $this->assertStringContainsString('The provider does not support this operation', $this->command->getDisplay());
+ }
+
+ public function testEnabled(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('belle')
+ ->willReturn($user);
+ $this->providerManager->expects($this->once())
+ ->method('tryEnableProviderFor')
+ ->with('totp', $user)
+ ->willReturn(true);
+
+ $rc = $this->command->execute([
+ 'uid' => 'belle',
+ 'provider_id' => 'totp',
+ ]);
+
+ $this->assertEquals(0, $rc);
+ $this->assertStringContainsString('Two-factor provider totp enabled for user belle', $this->command->getDisplay());
+ }
+}
diff --git a/tests/Core/Command/TwoFactorAuth/EnforceTest.php b/tests/Core/Command/TwoFactorAuth/EnforceTest.php
new file mode 100644
index 00000000000..03118772377
--- /dev/null
+++ b/tests/Core/Command/TwoFactorAuth/EnforceTest.php
@@ -0,0 +1,128 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Command\TwoFactorAuth;
+
+use OC\Authentication\TwoFactorAuth\EnforcementState;
+use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
+use OC\Core\Command\TwoFactorAuth\Enforce;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+class EnforceTest extends TestCase {
+ /** @var MandatoryTwoFactor|MockObject */
+ private $mandatoryTwoFactor;
+
+ /** @var CommandTester */
+ private $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class);
+ $command = new Enforce($this->mandatoryTwoFactor);
+
+ $this->command = new CommandTester($command);
+ }
+
+ public function testEnforce(): void {
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('setState')
+ ->with($this->equalTo(new EnforcementState(true)));
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('getState')
+ ->willReturn(new EnforcementState(true));
+
+ $rc = $this->command->execute([
+ '--on' => true,
+ ]);
+
+ $this->assertEquals(0, $rc);
+ $display = $this->command->getDisplay();
+ $this->assertStringContainsString('Two-factor authentication is enforced for all users', $display);
+ }
+
+ public function testEnforceForOneGroup(): void {
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('setState')
+ ->with($this->equalTo(new EnforcementState(true, ['twofactorers'])));
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('getState')
+ ->willReturn(new EnforcementState(true, ['twofactorers']));
+
+ $rc = $this->command->execute([
+ '--on' => true,
+ '--group' => ['twofactorers'],
+ ]);
+
+ $this->assertEquals(0, $rc);
+ $display = $this->command->getDisplay();
+ $this->assertStringContainsString('Two-factor authentication is enforced for members of the group(s) twofactorers', $display);
+ }
+
+ public function testEnforceForAllExceptOneGroup(): void {
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('setState')
+ ->with($this->equalTo(new EnforcementState(true, [], ['yoloers'])));
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('getState')
+ ->willReturn(new EnforcementState(true, [], ['yoloers']));
+
+ $rc = $this->command->execute([
+ '--on' => true,
+ '--exclude' => ['yoloers'],
+ ]);
+
+ $this->assertEquals(0, $rc);
+ $display = $this->command->getDisplay();
+ $this->assertStringContainsString('Two-factor authentication is enforced for all users, except members of yoloers', $display);
+ }
+
+ public function testDisableEnforced(): void {
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('setState')
+ ->with(new EnforcementState(false));
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('getState')
+ ->willReturn(new EnforcementState(false));
+
+ $rc = $this->command->execute([
+ '--off' => true,
+ ]);
+
+ $this->assertEquals(0, $rc);
+ $display = $this->command->getDisplay();
+ $this->assertStringContainsString('Two-factor authentication is not enforced', $display);
+ }
+
+ public function testCurrentStateEnabled(): void {
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('getState')
+ ->willReturn(new EnforcementState(true));
+
+ $rc = $this->command->execute([]);
+
+ $this->assertEquals(0, $rc);
+ $display = $this->command->getDisplay();
+ $this->assertStringContainsString('Two-factor authentication is enforced for all users', $display);
+ }
+
+ public function testCurrentStateDisabled(): void {
+ $this->mandatoryTwoFactor->expects($this->once())
+ ->method('getState')
+ ->willReturn(new EnforcementState(false));
+
+ $rc = $this->command->execute([]);
+
+ $this->assertEquals(0, $rc);
+ $display = $this->command->getDisplay();
+ $this->assertStringContainsString('Two-factor authentication is not enforced', $display);
+ }
+}
diff --git a/tests/Core/Command/TwoFactorAuth/StateTest.php b/tests/Core/Command/TwoFactorAuth/StateTest.php
new file mode 100644
index 00000000000..f4ca3c4e031
--- /dev/null
+++ b/tests/Core/Command/TwoFactorAuth/StateTest.php
@@ -0,0 +1,94 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Core\Command\TwoFactorAuth;
+
+use OC\Core\Command\TwoFactorAuth\State;
+use OCP\Authentication\TwoFactorAuth\IRegistry;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Tester\CommandTester;
+use Test\TestCase;
+
+class StateTest extends TestCase {
+ /** @var IRegistry|MockObject */
+ private $registry;
+
+ /** @var IUserManager|MockObject */
+ private $userManager;
+
+ /** @var CommandTester|MockObject */
+ private $cmd;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->registry = $this->createMock(IRegistry::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+
+ $cmd = new State($this->registry, $this->userManager);
+ $this->cmd = new CommandTester($cmd);
+ }
+
+ public function testWrongUID(): void {
+ $this->cmd->execute([
+ 'uid' => 'nope',
+ ]);
+
+ $output = $this->cmd->getDisplay();
+ $this->assertStringContainsString('Invalid UID', $output);
+ }
+
+ public function testStateNoProvidersActive(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('eldora')
+ ->willReturn($user);
+ $states = [
+ 'u2f' => false,
+ 'totp' => false,
+ ];
+ $this->registry->expects($this->once())
+ ->method('getProviderStates')
+ ->with($user)
+ ->willReturn($states);
+
+ $this->cmd->execute([
+ 'uid' => 'eldora',
+ ]);
+
+ $output = $this->cmd->getDisplay();
+ $this->assertStringContainsString('Two-factor authentication is not enabled for user eldora', $output);
+ }
+
+ public function testStateOneProviderActive(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('mohamed')
+ ->willReturn($user);
+ $states = [
+ 'u2f' => true,
+ 'totp' => false,
+ ];
+ $this->registry->expects($this->once())
+ ->method('getProviderStates')
+ ->with($user)
+ ->willReturn($states);
+
+ $this->cmd->execute([
+ 'uid' => 'mohamed',
+ ]);
+
+ $output = $this->cmd->getDisplay();
+ $this->assertStringContainsString('Two-factor authentication is enabled for user mohamed', $output);
+ }
+}
diff --git a/tests/Core/Command/User/AddTest.php b/tests/Core/Command/User/AddTest.php
new file mode 100644
index 00000000000..5a8bc3abea1
--- /dev/null
+++ b/tests/Core/Command/User/AddTest.php
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+declare(strict_types=1);
+
+namespace Core\Command\User;
+
+use OC\Core\Command\User\Add;
+use OCA\Settings\Mailer\NewUserMailHelper;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IAppConfig;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Mail\IEMailTemplate;
+use OCP\mail\IMailer;
+use OCP\Security\ISecureRandom;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class AddTest extends TestCase {
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $userManager;
+
+ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $groupManager;
+
+ /** @var IMailer|\PHPUnit\Framework\MockObject\MockObject */
+ private $mailer;
+
+ /** @var IAppConfig|\PHPUnit\Framework\MockObject\MockObject */
+ private $appConfig;
+
+ /** @var NewUserMailHelper|\PHPUnit\Framework\MockObject\MockObject */
+ private $mailHelper;
+
+ /** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
+ private $eventDispatcher;
+
+ /** @var ISecureRandom|\PHPUnit\Framework\MockObject\MockObject */
+ private $secureRandom;
+
+ /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */
+ private $user;
+
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $consoleInput;
+
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $consoleOutput;
+
+ /** @var Add */
+ private $addCommand;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = static::createMock(IUserManager::class);
+ $this->groupManager = static::createStub(IGroupManager::class);
+ $this->mailer = static::createMock(IMailer::class);
+ $this->appConfig = static::createMock(IAppConfig::class);
+ $this->mailHelper = static::createMock(NewUserMailHelper::class);
+ $this->eventDispatcher = static::createStub(IEventDispatcher::class);
+ $this->secureRandom = static::createStub(ISecureRandom::class);
+
+ $this->user = static::createMock(IUser::class);
+
+ $this->consoleInput = static::createMock(InputInterface::class);
+ $this->consoleOutput = static::createMock(OutputInterface::class);
+
+ $this->addCommand = new Add(
+ $this->userManager,
+ $this->groupManager,
+ $this->mailer,
+ $this->appConfig,
+ $this->mailHelper,
+ $this->eventDispatcher,
+ $this->secureRandom
+ );
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('addEmailDataProvider')]
+ public function testAddEmail(
+ ?string $email,
+ bool $isEmailValid,
+ bool $shouldSendEmail,
+ ): void {
+ $this->user->expects($isEmailValid ? static::once() : static::never())
+ ->method('setSystemEMailAddress')
+ ->with(static::equalTo($email));
+
+ $this->userManager->method('createUser')
+ ->willReturn($this->user);
+
+ $this->appConfig->method('getValueString')
+ ->willReturn($shouldSendEmail ? 'yes' : 'no');
+
+ $this->mailer->method('validateMailAddress')
+ ->willReturn($isEmailValid);
+
+ $this->mailHelper->method('generateTemplate')
+ ->willReturn(static::createMock(IEMailTemplate::class));
+
+ $this->mailHelper->expects($isEmailValid && $shouldSendEmail ? static::once() : static::never())
+ ->method('sendMail');
+
+ $this->consoleInput->method('getOption')
+ ->willReturnMap([
+ ['generate-password', 'true'],
+ ['email', $email],
+ ['group', []],
+ ]);
+
+ $this->invokePrivate($this->addCommand, 'execute', [
+ $this->consoleInput,
+ $this->consoleOutput
+ ]);
+ }
+
+ /**
+ * @return array
+ */
+ public static function addEmailDataProvider(): array {
+ return [
+ 'Valid E-Mail' => [
+ 'info@example.com',
+ true,
+ true,
+ ],
+ 'Invalid E-Mail' => [
+ 'info@@example.com',
+ false,
+ false,
+ ],
+ 'No E-Mail' => [
+ '',
+ false,
+ false,
+ ],
+ 'Valid E-Mail, but no mail should be sent' => [
+ 'info@example.com',
+ true,
+ false,
+ ],
+ ];
+ }
+}
diff --git a/tests/Core/Command/User/AuthTokens/DeleteTest.php b/tests/Core/Command/User/AuthTokens/DeleteTest.php
new file mode 100644
index 00000000000..6692473c240
--- /dev/null
+++ b/tests/Core/Command/User/AuthTokens/DeleteTest.php
@@ -0,0 +1,166 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace Tests\Core\Command\User\AuthTokens;
+
+use OC\Authentication\Token\IProvider;
+use OC\Core\Command\User\AuthTokens\Delete;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\RuntimeException;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DeleteTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $tokenProvider;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $tokenProvider = $this->tokenProvider = $this->getMockBuilder(IProvider::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var \OC\Authentication\Token\IProvider $tokenProvider */
+ $this->command = new Delete($tokenProvider);
+ }
+
+ public function testDeleteTokenById(): void {
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'user'],
+ ['id', '42']
+ ]);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getOption')
+ ->with('last-used-before')
+ ->willReturn(null);
+
+ $this->tokenProvider->expects($this->once())
+ ->method('invalidateTokenById')
+ ->with('user', 42);
+
+ $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ $this->assertSame(Command::SUCCESS, $result);
+ }
+
+ public function testDeleteTokenByIdRequiresTokenId(): void {
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'user'],
+ ['id', null]
+ ]);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getOption')
+ ->with('last-used-before')
+ ->willReturn(null);
+
+ $this->expectException(RuntimeException::class);
+
+ $this->tokenProvider->expects($this->never())->method('invalidateTokenById');
+
+ $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ $this->assertSame(Command::FAILURE, $result);
+ }
+
+ public function testDeleteTokensLastUsedBefore(): void {
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'user'],
+ ['id', null]
+ ]);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getOption')
+ ->with('last-used-before')
+ ->willReturn('946684800');
+
+ $this->tokenProvider->expects($this->once())
+ ->method('invalidateLastUsedBefore')
+ ->with('user', 946684800);
+
+ $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ $this->assertSame(Command::SUCCESS, $result);
+ }
+
+ public function testLastUsedBeforeAcceptsIso8601Expanded(): void {
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'user'],
+ ['id', null]
+ ]);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getOption')
+ ->with('last-used-before')
+ ->willReturn('2000-01-01T00:00:00Z');
+
+ $this->tokenProvider->expects($this->once())
+ ->method('invalidateLastUsedBefore')
+ ->with('user', 946684800);
+
+ $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ $this->assertSame(Command::SUCCESS, $result);
+ }
+
+ public function testLastUsedBeforeAcceptsYmd(): void {
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'user'],
+ ['id', null]
+ ]);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getOption')
+ ->with('last-used-before')
+ ->willReturn('2000-01-01');
+
+ $this->tokenProvider->expects($this->once())
+ ->method('invalidateLastUsedBefore')
+ ->with('user', 946684800);
+
+ $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ $this->assertSame(Command::SUCCESS, $result);
+ }
+
+ public function testIdAndLastUsedBeforeAreMutuallyExclusive(): void {
+ $this->consoleInput->expects($this->exactly(2))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'user'],
+ ['id', '42']
+ ]);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getOption')
+ ->with('last-used-before')
+ ->willReturn('946684800');
+
+ $this->expectException(RuntimeException::class);
+
+ $this->tokenProvider->expects($this->never())->method('invalidateLastUsedBefore');
+
+ $result = self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ $this->assertSame(Command::SUCCESS, $result);
+ }
+}
diff --git a/tests/Core/Command/User/DeleteTest.php b/tests/Core/Command/User/DeleteTest.php
new file mode 100644
index 00000000000..4e06b0f91fc
--- /dev/null
+++ b/tests/Core/Command/User/DeleteTest.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\User;
+
+use OC\Core\Command\User\Delete;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DeleteTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $userManager;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $userManager = $this->userManager = $this->getMockBuilder(IUserManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var IUserManager $userManager */
+ $this->command = new Delete($userManager);
+ }
+
+
+ public static function validUserLastSeen(): array {
+ return [
+ [true, 'The specified user was deleted'],
+ [false, 'The specified user could not be deleted'],
+ ];
+ }
+
+ /**
+ *
+ * @param bool $deleteSuccess
+ * @param string $expectedString
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('validUserLastSeen')]
+ public function testValidUser($deleteSuccess, $expectedString): void {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $user->expects($this->once())
+ ->method('delete')
+ ->willReturn($deleteSuccess);
+
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('uid')
+ ->willReturn('user');
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains($expectedString));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testInvalidUser(): void {
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('user')
+ ->willReturn(null);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('uid')
+ ->willReturn('user');
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains('User does not exist'));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/User/DisableTest.php b/tests/Core/Command/User/DisableTest.php
new file mode 100644
index 00000000000..c1bc10dc6bf
--- /dev/null
+++ b/tests/Core/Command/User/DisableTest.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Command\User;
+
+use OC\Core\Command\User\Disable;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class DisableTest extends TestCase {
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ protected $userManager;
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var Disable */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+
+ $this->command = new Disable($this->userManager);
+ }
+
+ public function testValidUser(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('setEnabled')
+ ->with(false);
+
+ $this->userManager
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+
+ $this->consoleInput
+ ->method('getArgument')
+ ->with('uid')
+ ->willReturn('user');
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains('The specified user is disabled'));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testInvalidUser(): void {
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('user')
+ ->willReturn(null);
+
+ $this->consoleInput
+ ->method('getArgument')
+ ->with('uid')
+ ->willReturn('user');
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains('User does not exist'));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/User/EnableTest.php b/tests/Core/Command/User/EnableTest.php
new file mode 100644
index 00000000000..b2820de14ef
--- /dev/null
+++ b/tests/Core/Command/User/EnableTest.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Command\User;
+
+use OC\Core\Command\User\Enable;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class EnableTest extends TestCase {
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ protected $userManager;
+ /** @var InputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var OutputInterface|\PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var Disable */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+
+ $this->command = new Enable($this->userManager);
+ }
+
+ public function testValidUser(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('setEnabled')
+ ->with(true);
+
+ $this->userManager
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+
+ $this->consoleInput
+ ->method('getArgument')
+ ->with('uid')
+ ->willReturn('user');
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains('The specified user is enabled'));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testInvalidUser(): void {
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('user')
+ ->willReturn(null);
+
+ $this->consoleInput
+ ->method('getArgument')
+ ->with('uid')
+ ->willReturn('user');
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains('User does not exist'));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/User/LastSeenTest.php b/tests/Core/Command/User/LastSeenTest.php
new file mode 100644
index 00000000000..64c710eacc5
--- /dev/null
+++ b/tests/Core/Command/User/LastSeenTest.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\User;
+
+use OC\Core\Command\User\LastSeen;
+use OCP\IUser;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class LastSeenTest extends TestCase {
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $userManager;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleInput;
+ /** @var \PHPUnit\Framework\MockObject\MockObject */
+ protected $consoleOutput;
+
+ /** @var \Symfony\Component\Console\Command\Command */
+ protected $command;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $userManager = $this->userManager = $this->getMockBuilder(IUserManager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->consoleInput = $this->getMockBuilder(InputInterface::class)->getMock();
+ $this->consoleOutput = $this->getMockBuilder(OutputInterface::class)->getMock();
+
+ /** @var IUserManager $userManager */
+ $this->command = new LastSeen($userManager);
+ }
+
+ public static function validUserLastSeen(): array {
+ return [
+ [0, 'never logged in'],
+ [time(), 'last login'],
+ ];
+ }
+
+ /**
+ *
+ * @param int $lastSeen
+ * @param string $expectedString
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('validUserLastSeen')]
+ public function testValidUser($lastSeen, $expectedString): void {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $user->expects($this->once())
+ ->method('getLastLogin')
+ ->willReturn($lastSeen);
+
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('user')
+ ->willReturn($user);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('uid')
+ ->willReturn('user');
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains($expectedString));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+
+ public function testInvalidUser(): void {
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->with('user')
+ ->willReturn(null);
+
+ $this->consoleInput->expects($this->once())
+ ->method('getArgument')
+ ->with('uid')
+ ->willReturn('user');
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($this->stringContains('User does not exist'));
+
+ self::invokePrivate($this->command, 'execute', [$this->consoleInput, $this->consoleOutput]);
+ }
+}
diff --git a/tests/Core/Command/User/ProfileTest.php b/tests/Core/Command/User/ProfileTest.php
new file mode 100644
index 00000000000..ff5568bacfc
--- /dev/null
+++ b/tests/Core/Command/User/ProfileTest.php
@@ -0,0 +1,465 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace Core\Command\User;
+
+use OC\Core\Command\User\Profile;
+use OCP\Accounts\IAccount;
+use OCP\Accounts\IAccountManager;
+use OCP\Accounts\IAccountProperty;
+use OCP\IDBConnection;
+use OCP\IUser;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class ProfileTest extends TestCase {
+
+ protected IAccountManager&MockObject $accountManager;
+ protected IUserManager&MockObject $userManager;
+ protected IDBConnection&MockObject $connection;
+ protected InputInterface&MockObject $consoleInput;
+ protected OutputInterface&MockObject $consoleOutput;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->accountManager = $this->createMock(IAccountManager::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->connection = $this->createMock(IDBConnection::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+ }
+
+ public function getCommand(array $methods = []): Profile|MockObject {
+ if (empty($methods)) {
+ return new Profile($this->userManager, $this->accountManager);
+ } else {
+ return $this->getMockBuilder(Profile::class)
+ ->setConstructorArgs([
+ $this->userManager,
+ $this->accountManager,
+ ])
+ ->onlyMethods($methods)
+ ->getMock();
+ }
+ }
+
+ public static function dataCheckInput(): array {
+ return [
+ 'Call with existing user should pass check' => [
+ [['uid', 'username']],
+ [],
+ [],
+ true,
+ null,
+ ],
+ 'Call with non-existing user should fail check' => [
+ [['uid', 'username']],
+ [],
+ [],
+ false,
+ 'The user "username" does not exist.',
+ ],
+
+ 'Call with uid, key and --default value should pass check' => [
+ [['uid', 'username'], ['key', 'configkey']],
+ [],
+ [['--default-value', false, true]],
+ true,
+ null,
+ ],
+ 'Call with uid and empty key with default-value option should fail check' => [
+ [['uid', 'username'], ['key', '']],
+ [],
+ [['--default-value', false, true]],
+ true,
+ 'The "default-value" option can only be used when specifying a key.',
+ ],
+
+ 'Call with uid, key, value should pass check' => [
+ [['uid', 'username'], ['key', 'configkey'], ['value', '']],
+ [],
+ [],
+ true,
+ null,
+ ],
+ 'Call with uid, empty key and empty value should fail check' => [
+ [['uid', 'username'], ['key', ''], ['value', '']],
+ [],
+ [],
+ true,
+ 'The value argument can only be used when specifying a key.',
+ ],
+ 'Call with uid, key, empty value and default-value option should fail check' => [
+ [['uid', 'username'], ['key', 'configkey'], ['value', '']],
+ [],
+ [['--default-value', false, true]],
+ true,
+ 'The value argument can not be used together with "default-value".',
+ ],
+ 'Call with uid, key, empty value and update-only option should pass check' => [
+ [['uid', 'username'], ['key', 'configkey'], ['value', '']],
+ [['update-only', true]],
+ [],
+ true,
+ null,
+ ],
+ 'Call with uid, key, null value and update-only option should fail check' => [
+ [['uid', 'username'], ['key', 'configkey'], ['value', null]],
+ [['update-only', true]],
+ [],
+ true,
+ 'The "update-only" option can only be used together with "value".',
+ ],
+
+ 'Call with uid, key and delete option should pass check' => [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['delete', true]],
+ [],
+ true,
+ null,
+ ],
+ 'Call with uid, empty key and delete option should fail check' => [
+ [['uid', 'username'], ['key', '']],
+ [['delete', true]],
+ [],
+ true,
+ 'The "delete" option can only be used when specifying a key.',
+ ],
+ 'Call with uid, key, delete option and default-value should fail check' => [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['delete', true]],
+ [['--default-value', false, true]],
+ true,
+ 'The "delete" option can not be used together with "default-value".',
+ ],
+ 'Call with uid, key, empty value and delete option should fail check' => [
+ [['uid', 'username'], ['key', 'configkey'], ['value', '']],
+ [['delete', true]],
+ [],
+ true,
+ 'The "delete" option can not be used together with "value".',
+ ],
+ 'Call with uid, key, delete and error-if-not-exists should pass check' => [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['delete', true], ['error-if-not-exists', true]],
+ [],
+ true,
+ null,
+ ],
+ 'Call with uid, key and error-if-not-exists should fail check' => [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['delete', false], ['error-if-not-exists', true]],
+ [],
+ true,
+ 'The "error-if-not-exists" option can only be used together with "delete".',
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCheckInput')]
+ public function testCheckInput(array $arguments, array $options, array $parameterOptions, bool $existingUser, ?string $expectedException): void {
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->willReturnMap($arguments);
+ $this->consoleInput->expects($this->any())
+ ->method('getOption')
+ ->willReturnMap($options);
+ $this->consoleInput->expects($this->any())
+ ->method('hasParameterOption')
+ ->willReturnCallback(function (string|array $values, bool $onlyParams = false) use ($parameterOptions): bool {
+ $arguments = func_get_args();
+ foreach ($parameterOptions as $parameterOption) {
+ // check the arguments of the function, if they are the same, return the mocked value
+ if (array_diff($arguments, $parameterOption) === []) {
+ return end($parameterOption);
+ }
+ }
+
+ return false;
+ });
+
+ $returnedUser = null;
+ if ($existingUser) {
+ $mockUser = $this->createMock(IUser::class);
+ $mockUser->expects($this->once())->method('getUID')->willReturn('user');
+ $returnedUser = $mockUser;
+ }
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->willReturn($returnedUser);
+
+ $command = $this->getCommand();
+ try {
+ $this->invokePrivate($command, 'checkInput', [$this->consoleInput]);
+ $this->assertNull($expectedException);
+ } catch (\InvalidArgumentException $e) {
+ $this->assertEquals($expectedException, $e->getMessage());
+ }
+ }
+
+ public function testCheckInputExceptionCatch(): void {
+ $command = $this->getCommand(['checkInput']);
+ $command->expects($this->once())
+ ->method('checkInput')
+ ->willThrowException(new \InvalidArgumentException('test'));
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with('<error>test</error>');
+
+ $this->assertEquals(1, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public static function dataExecuteDeleteProfileProperty(): array {
+ return [
+ 'Deleting existing property should succeed' => ['address', 'Berlin', false, null, Command::SUCCESS],
+ 'Deleting existing property with error-if-not-exists should succeed' => ['address', 'Berlin', true, null, Command::SUCCESS],
+ 'Deleting non-existing property should succeed' => ['address', '', false, null, Command::SUCCESS],
+ 'Deleting non-existing property with error-if-not-exists should fail' => ['address', '', true, '<error>The property does not exist for user "username".</error>', Command::FAILURE],
+ ];
+ }
+
+ /**
+ * Tests the deletion mechanism on profile settings.
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteDeleteProfileProperty')]
+ public function testExecuteDeleteProfileProperty(string $configKey, string $value, bool $errorIfNotExists, ?string $expectedLine, int $expectedReturn): void {
+ $uid = 'username';
+ $appName = 'profile';
+ $command = $this->getCommand([
+ 'writeArrayInOutputFormat',
+ 'checkInput',
+ ]);
+
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', $uid],
+ ['app', $appName],
+ ['key', $configKey],
+ ]);
+
+ $mocks = $this->setupProfilePropertiesMock([$configKey => $value]);
+
+ $command->expects($this->once())
+ ->method('checkInput')
+ ->willReturn($mocks['userMock']);
+
+ $this->consoleInput->expects($this->atLeastOnce())
+ ->method('hasParameterOption')
+ ->willReturnMap([
+ ['--delete', false, true],
+ ['--error-if-not-exists', false, $errorIfNotExists],
+ ]);
+
+ if ($expectedLine === null) {
+ $this->consoleOutput->expects($this->never())
+ ->method('writeln');
+ $mocks['profilePropertiesMocks'][0]->expects($this->once())
+ ->method('setValue')
+ ->with('');
+ $this->accountManager->expects($this->once())
+ ->method('updateAccount')
+ ->with($mocks['accountMock']);
+ } else {
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($expectedLine);
+ $this->accountManager->expects($this->never())
+ ->method('updateAccount');
+ }
+
+ $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public function testExecuteSetProfileProperty(): void {
+ $command = $this->getCommand([
+ 'writeArrayInOutputFormat',
+ 'checkInput',
+ ]);
+
+ $uid = 'username';
+ $propertyKey = 'address';
+ $propertyValue = 'Barcelona';
+
+ $this->consoleInput->expects($this->atLeast(3))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', $uid],
+ ['key', $propertyKey],
+ ['value', $propertyValue],
+ ]);
+
+ $mocks = $this->setupProfilePropertiesMock([$propertyKey => $propertyValue]);
+
+ $command->expects($this->once())
+ ->method('checkInput')
+ ->willReturn($mocks['userMock']);
+
+ $mocks['profilePropertiesMocks'][0]->expects($this->once())
+ ->method('setValue')
+ ->with($propertyValue);
+ $this->accountManager->expects($this->once())
+ ->method('updateAccount')
+ ->with($mocks['accountMock']);
+
+ $this->assertEquals(0, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public static function dataExecuteGet(): array {
+ return [
+ 'Get property with set value should pass' => ['configkey', 'value', null, 'value', Command::SUCCESS],
+ 'Get property with empty value and default-value option should pass' => ['configkey', '', 'default-value', 'default-value', Command::SUCCESS],
+ 'Get property with empty value should fail' => ['configkey', '', null, '<error>The property does not exist for user "username".</error>', Command::FAILURE],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteGet')]
+ public function testExecuteGet(string $key, string $value, ?string $defaultValue, string $expectedLine, int $expectedReturn): void {
+ $command = $this->getCommand([
+ 'writeArrayInOutputFormat',
+ 'checkInput',
+ ]);
+
+ $uid = 'username';
+
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', $uid],
+ ['key', $key],
+ ]);
+
+ $mocks = $this->setupProfilePropertiesMock([$key => $value]);
+
+ $command->expects($this->once())
+ ->method('checkInput')
+ ->willReturn($mocks['userMock']);
+
+ if ($value === '') {
+ if ($defaultValue === null) {
+ $this->consoleInput->expects($this->atLeastOnce())
+ ->method('hasParameterOption')
+ ->willReturn(false);
+ } else {
+ $this->consoleInput->expects($this->atLeastOnce())
+ ->method('hasParameterOption')
+ ->willReturnCallback(fn (string|array $values): bool => $values === '--default-value');
+ $this->consoleInput->expects($this->once())
+ ->method('getOption')
+ ->with('default-value')
+ ->willReturn($defaultValue);
+ }
+ }
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($expectedLine);
+
+ $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public function testExecuteList(): void {
+ $uid = 'username';
+ $profileData = [
+ 'pronouns' => 'they/them',
+ 'address' => 'Berlin',
+ ];
+
+ $command = $this->getCommand([
+ 'writeArrayInOutputFormat',
+ 'checkInput',
+ ]);
+
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', $uid],
+ ['key', ''],
+ ]);
+
+ $mocks = $this->setupProfilePropertiesMock(['address' => $profileData['address'], 'pronouns' => $profileData['pronouns']]);
+
+ $command->expects($this->once())
+ ->method('checkInput')
+ ->willReturn($mocks['userMock']);
+
+ $command->expects($this->once())
+ ->method('writeArrayInOutputFormat')
+ ->with($this->consoleInput, $this->consoleOutput, $profileData);
+
+
+ $this->assertEquals(0, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ /**
+ * Helper to avoid boilerplate in tests in this file when mocking objects
+ * of IAccountProperty type.
+ *
+ * @param array<string, string> $properties the properties to be set up as key => value
+ * @return array{
+ * userMock: IUser&MockObject,
+ * accountMock: IAccount&MockObject,
+ * profilePropertiesMocks: IAccountProperty&MockObject[]
+ * }
+ */
+ private function setupProfilePropertiesMock(array $properties): array {
+ $userMock = $this->createMock(IUser::class);
+ $accountMock = $this->createMock(IAccount::class);
+ $this->accountManager->expects($this->atLeastOnce())
+ ->method('getAccount')
+ ->with($userMock)
+ ->willReturn($accountMock);
+
+ /** @var IAccountProperty&MockObject[] $propertiesMocks */
+ $propertiesMocks = [];
+ foreach ($properties as $key => $value) {
+ $propertiesMocks[] = $this->getAccountPropertyMock($key, $value);
+ }
+
+ if (count($properties) === 1) {
+ $accountMock->expects($this->atLeastOnce())
+ ->method('getProperty')
+ ->with(array_keys($properties)[0])
+ ->willReturn($propertiesMocks[array_key_first($propertiesMocks)]);
+ } else {
+ $accountMock->expects($this->atLeastOnce())
+ ->method('getAllProperties')
+ ->willReturnCallback(function () use ($propertiesMocks) {
+ foreach ($propertiesMocks as $property) {
+ yield $property;
+ }
+ });
+ }
+
+ return [
+ 'userMock' => $userMock,
+ 'accountMock' => $accountMock,
+ 'profilePropertiesMocks' => $propertiesMocks,
+ ];
+ }
+
+ private function getAccountPropertyMock(string $name, string $value): IAccountProperty&MockObject {
+ $propertyMock = $this->getMockBuilder(IAccountProperty::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $propertyMock->expects($this->any())
+ ->method('getName')
+ ->willReturn($name);
+ $propertyMock->expects($this->any())
+ ->method('getValue')
+ ->willReturn($value);
+
+ return $propertyMock;
+ }
+}
diff --git a/tests/Core/Command/User/SettingTest.php b/tests/Core/Command/User/SettingTest.php
new file mode 100644
index 00000000000..706e5b24742
--- /dev/null
+++ b/tests/Core/Command/User/SettingTest.php
@@ -0,0 +1,452 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Command\User;
+
+use InvalidArgumentException;
+use OC\Core\Command\User\Setting;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IUserManager;
+use PHPUnit\Framework\MockObject\MockObject;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Test\TestCase;
+
+class SettingTest extends TestCase {
+ protected IUserManager&MockObject $userManager;
+ protected IConfig&MockObject $config;
+ protected IDBConnection&MockObject $connection;
+ protected InputInterface&MockObject $consoleInput;
+ protected MockObject&OutputInterface $consoleOutput;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->connection = $this->createMock(IDBConnection::class);
+ $this->consoleInput = $this->createMock(InputInterface::class);
+ $this->consoleOutput = $this->createMock(OutputInterface::class);
+ }
+
+ public function getCommand(array $methods = []) {
+ if (empty($methods)) {
+ return new Setting($this->userManager, $this->config);
+ } else {
+ $mock = $this->getMockBuilder(Setting::class)
+ ->setConstructorArgs([
+ $this->userManager,
+ $this->config,
+ ])
+ ->onlyMethods($methods)
+ ->getMock();
+ return $mock;
+ }
+ }
+
+ public static function dataCheckInput(): array {
+ return [
+ [
+ [['uid', 'username']],
+ [['ignore-missing-user', true]],
+ [],
+ false,
+ false,
+ ],
+ [
+ [['uid', 'username']],
+ [['ignore-missing-user', false]],
+ [],
+ null,
+ 'The user "username" does not exist.',
+ ],
+
+ [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['ignore-missing-user', true]],
+ [['--default-value', false, true]],
+ false,
+ false,
+ ],
+ [
+ [['uid', 'username'], ['key', '']],
+ [['ignore-missing-user', true]],
+ [['--default-value', false, true]],
+ false,
+ 'The "default-value" option can only be used when specifying a key.',
+ ],
+
+ [
+ [['uid', 'username'], ['key', 'configkey'], ['value', '']],
+ [['ignore-missing-user', true]],
+ [],
+ false,
+ false,
+ ],
+ [
+ [['uid', 'username'], ['key', ''], ['value', '']],
+ [['ignore-missing-user', true]],
+ [],
+ false,
+ 'The value argument can only be used when specifying a key.',
+ ],
+ [
+ [['uid', 'username'], ['key', 'configkey'], ['value', '']],
+ [['ignore-missing-user', true]],
+ [['--default-value', false, true]],
+ false,
+ 'The value argument can not be used together with "default-value".',
+ ],
+ [
+ [['uid', 'username'], ['key', 'configkey'], ['value', '']],
+ [['ignore-missing-user', true], ['update-only', true]],
+ [],
+ false,
+ false,
+ ],
+ [
+ [['uid', 'username'], ['key', 'configkey'], ['value', null]],
+ [['ignore-missing-user', true], ['update-only', true]],
+ [],
+ false,
+ 'The "update-only" option can only be used together with "value".',
+ ],
+
+ [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['ignore-missing-user', true], ['delete', true]],
+ [],
+ false,
+ false,
+ ],
+ [
+ [['uid', 'username'], ['key', '']],
+ [['ignore-missing-user', true], ['delete', true]],
+ [],
+ false,
+ 'The "delete" option can only be used when specifying a key.',
+ ],
+ [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['ignore-missing-user', true], ['delete', true]],
+ [['--default-value', false, true]],
+ false,
+ 'The "delete" option can not be used together with "default-value".',
+ ],
+ [
+ [['uid', 'username'], ['key', 'configkey'], ['value', '']],
+ [['ignore-missing-user', true], ['delete', true]],
+ [],
+ false,
+ 'The "delete" option can not be used together with "value".',
+ ],
+ [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['ignore-missing-user', true], ['delete', true], ['error-if-not-exists', true]],
+ [],
+ false,
+ false,
+ ],
+ [
+ [['uid', 'username'], ['key', 'configkey']],
+ [['ignore-missing-user', true], ['delete', false], ['error-if-not-exists', true]],
+ [],
+ false,
+ 'The "error-if-not-exists" option can only be used together with "delete".',
+ ],
+ ];
+ }
+
+ /**
+ *
+ * @param array $arguments
+ * @param array $options
+ * @param array $parameterOptions
+ * @param mixed $user
+ * @param string $expectedException
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataCheckInput')]
+ public function testCheckInput($arguments, $options, $parameterOptions, $user, $expectedException): void {
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->willReturnMap($arguments);
+ $this->consoleInput->expects($this->any())
+ ->method('getOption')
+ ->willReturnMap($options);
+ $this->consoleInput->expects($this->any())
+ ->method('hasParameterOption')
+ ->willReturnCallback(function (string|array $config, bool $default = false) use ($parameterOptions): bool {
+ foreach ($parameterOptions as $parameterOption) {
+ if ($config === $parameterOption[0]
+ // Check the default value if the maps has 3 entries
+ && (!isset($parameterOption[2]) || $default === $parameterOption[1])) {
+ return end($parameterOption);
+ }
+ }
+ return false;
+ });
+
+ if ($user !== false) {
+ $this->userManager->expects($this->once())
+ ->method('get')
+ ->willReturn($user);
+ } else {
+ $this->userManager->expects($this->never())
+ ->method('get');
+ }
+
+ $command = $this->getCommand();
+ try {
+ $this->invokePrivate($command, 'checkInput', [$this->consoleInput]);
+ $this->assertFalse($expectedException);
+ } catch (InvalidArgumentException $e) {
+ $this->assertEquals($expectedException, $e->getMessage());
+ }
+ }
+
+ public function testCheckInputExceptionCatch(): void {
+ $command = $this->getCommand(['checkInput']);
+ $command->expects($this->once())
+ ->method('checkInput')
+ ->willThrowException(new InvalidArgumentException('test'));
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with('<error>test</error>');
+
+ $this->assertEquals(1, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public static function dataExecuteDelete(): array {
+ return [
+ ['config', false, null, 0],
+ ['config', true, null, 0],
+ [null, false, null, 0],
+ [null, true, '<error>The setting does not exist for user "username".</error>', 1],
+ ];
+ }
+
+ /**
+ *
+ * @param string|null $value
+ * @param bool $errorIfNotExists
+ * @param string $expectedLine
+ * @param int $expectedReturn
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteDelete')]
+ public function testExecuteDelete($value, $errorIfNotExists, $expectedLine, $expectedReturn): void {
+ $command = $this->getCommand([
+ 'writeArrayInOutputFormat',
+ 'checkInput',
+ 'getUserSettings',
+ ]);
+
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'username'],
+ ['app', 'appname'],
+ ['key', 'configkey'],
+ ]);
+
+ $command->expects($this->once())
+ ->method('checkInput');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('username', 'appname', 'configkey', null)
+ ->willReturn($value);
+
+ $this->consoleInput->expects($this->atLeastOnce())
+ ->method('hasParameterOption')
+ ->willReturnMap([
+ ['--delete', false, true],
+ ['--error-if-not-exists', false, $errorIfNotExists],
+ ]);
+
+ if ($expectedLine === null) {
+ $this->consoleOutput->expects($this->never())
+ ->method('writeln');
+ $this->config->expects($this->once())
+ ->method('deleteUserValue')
+ ->with('username', 'appname', 'configkey');
+ } else {
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($expectedLine);
+ $this->config->expects($this->never())
+ ->method('deleteUserValue');
+ }
+
+ $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public static function dataExecuteSet(): array {
+ return [
+ ['config', false, null, 0],
+ ['config', true, null, 0],
+ [null, false, null, 0],
+ [null, true, '<error>The setting does not exist for user "username".</error>', 1],
+ ];
+ }
+
+ /**
+ *
+ * @param string|null $value
+ * @param bool $updateOnly
+ * @param string $expectedLine
+ * @param int $expectedReturn
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteSet')]
+ public function testExecuteSet($value, $updateOnly, $expectedLine, $expectedReturn): void {
+ $command = $this->getCommand([
+ 'writeArrayInOutputFormat',
+ 'checkInput',
+ 'getUserSettings',
+ ]);
+
+ $this->consoleInput->expects($this->atLeast(4))
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'username'],
+ ['app', 'appname'],
+ ['key', 'configkey'],
+ ['value', 'setValue'],
+ ]);
+
+ $command->expects($this->once())
+ ->method('checkInput');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('username', 'appname', 'configkey', null)
+ ->willReturn($value);
+
+ $this->consoleInput->expects($this->atLeastOnce())
+ ->method('hasParameterOption')
+ ->willReturnMap([
+ ['--update-only', false, $updateOnly],
+ ]);
+
+ if ($expectedLine === null) {
+ $this->consoleOutput->expects($this->never())
+ ->method('writeln');
+
+ $this->consoleInput->expects($this->never())
+ ->method('getOption');
+
+ $this->config->expects($this->once())
+ ->method('setUserValue')
+ ->with('username', 'appname', 'configkey', 'setValue');
+ } else {
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($expectedLine);
+ $this->config->expects($this->never())
+ ->method('setUserValue');
+ }
+
+ $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public static function dataExecuteGet(): array {
+ return [
+ ['config', null, 'config', 0],
+ [null, 'config', 'config', 0],
+ [null, null, '<error>The setting does not exist for user "username".</error>', 1],
+ ];
+ }
+
+ /**
+ *
+ * @param string|null $value
+ * @param string|null $defaultValue
+ * @param string $expectedLine
+ * @param int $expectedReturn
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataExecuteGet')]
+ public function testExecuteGet($value, $defaultValue, $expectedLine, $expectedReturn): void {
+ $command = $this->getCommand([
+ 'writeArrayInOutputFormat',
+ 'checkInput',
+ 'getUserSettings',
+ ]);
+
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'username'],
+ ['app', 'appname'],
+ ['key', 'configkey'],
+ ]);
+
+ $command->expects($this->once())
+ ->method('checkInput');
+
+ $this->config->expects($this->once())
+ ->method('getUserValue')
+ ->with('username', 'appname', 'configkey', null)
+ ->willReturn($value);
+
+ if ($value === null) {
+ if ($defaultValue === null) {
+ $this->consoleInput->expects($this->atLeastOnce())
+ ->method('hasParameterOption')
+ ->willReturn(false);
+ } else {
+ $this->consoleInput->expects($this->atLeastOnce())
+ ->method('hasParameterOption')
+ ->willReturnCallback(function (string|array $config, bool $default = false): bool {
+ if ($config === '--default-value' && $default === false) {
+ return true;
+ }
+ return false;
+ });
+ $this->consoleInput->expects($this->once())
+ ->method('getOption')
+ ->with('default-value')
+ ->willReturn($defaultValue);
+ }
+ }
+
+ $this->consoleOutput->expects($this->once())
+ ->method('writeln')
+ ->with($expectedLine);
+
+ $this->assertEquals($expectedReturn, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+
+ public function testExecuteList(): void {
+ $command = $this->getCommand([
+ 'writeArrayInOutputFormat',
+ 'checkInput',
+ 'getUserSettings',
+ ]);
+
+ $this->consoleInput->expects($this->any())
+ ->method('getArgument')
+ ->willReturnMap([
+ ['uid', 'username'],
+ ['app', 'appname'],
+ ['key', ''],
+ ]);
+
+ $command->expects($this->once())
+ ->method('checkInput');
+ $command->expects($this->once())
+ ->method('getUserSettings')
+ ->willReturn(['settings']);
+ $command->expects($this->once())
+ ->method('writeArrayInOutputFormat')
+ ->with($this->consoleInput, $this->consoleOutput, ['settings']);
+
+
+ $this->assertEquals(0, $this->invokePrivate($command, 'execute', [$this->consoleInput, $this->consoleOutput]));
+ }
+}
diff --git a/tests/Core/Controller/AppPasswordControllerTest.php b/tests/Core/Controller/AppPasswordControllerTest.php
new file mode 100644
index 00000000000..eb1566eca8b
--- /dev/null
+++ b/tests/Core/Controller/AppPasswordControllerTest.php
@@ -0,0 +1,249 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OC\Core\Controller\AppPasswordController;
+use OC\User\Session;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCS\OCSForbiddenException;
+use OCP\Authentication\Exceptions\CredentialsUnavailableException;
+use OCP\Authentication\Exceptions\PasswordUnavailableException;
+use OCP\Authentication\LoginCredentials\ICredentials;
+use OCP\Authentication\LoginCredentials\IStore;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IUserManager;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class AppPasswordControllerTest extends TestCase {
+ /** @var ISession|MockObject */
+ private $session;
+
+ /** @var ISecureRandom|MockObject */
+ private $random;
+
+ /** @var IProvider|MockObject */
+ private $tokenProvider;
+
+ /** @var IStore|MockObject */
+ private $credentialStore;
+
+ /** @var IRequest|MockObject */
+ private $request;
+
+ /** @var IEventDispatcher|\PHPUnit\Framework\MockObject\MockObject */
+ private $eventDispatcher;
+
+ /** @var Session|MockObject */
+ private $userSession;
+
+ /** @var IUserManager|MockObject */
+ private $userManager;
+
+ /** @var IThrottler|MockObject */
+ private $throttler;
+
+ /** @var AppPasswordController */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->session = $this->createMock(ISession::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->tokenProvider = $this->createMock(IProvider::class);
+ $this->credentialStore = $this->createMock(IStore::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->userSession = $this->createMock(Session::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+
+ $this->controller = new AppPasswordController(
+ 'core',
+ $this->request,
+ $this->session,
+ $this->random,
+ $this->tokenProvider,
+ $this->credentialStore,
+ $this->eventDispatcher,
+ $this->userSession,
+ $this->userManager,
+ $this->throttler
+ );
+ }
+
+ public function testGetAppPasswordWithAppPassword(): void {
+ $this->session->method('exists')
+ ->with('app_password')
+ ->willReturn(true);
+
+ $this->expectException(OCSForbiddenException::class);
+
+ $this->controller->getAppPassword();
+ }
+
+ public function testGetAppPasswordNoLoginCreds(): void {
+ $this->session->method('exists')
+ ->with('app_password')
+ ->willReturn(false);
+ $this->credentialStore->method('getLoginCredentials')
+ ->willThrowException(new CredentialsUnavailableException());
+
+ $this->expectException(OCSForbiddenException::class);
+
+ $this->controller->getAppPassword();
+ }
+
+ public function testGetAppPassword(): void {
+ $credentials = $this->createMock(ICredentials::class);
+
+ $this->session->method('exists')
+ ->with('app_password')
+ ->willReturn(false);
+ $this->credentialStore->method('getLoginCredentials')
+ ->willReturn($credentials);
+ $credentials->method('getUid')
+ ->willReturn('myUID');
+ $credentials->method('getPassword')
+ ->willReturn('myPassword');
+ $credentials->method('getLoginName')
+ ->willReturn('myLoginName');
+ $this->request->method('getHeader')
+ ->with('user-agent')
+ ->willReturn('myUA');
+ $this->random->method('generate')
+ ->with(
+ 72,
+ ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS
+ )->willReturn('myToken');
+
+ $this->tokenProvider->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'myToken',
+ 'myUID',
+ 'myLoginName',
+ 'myPassword',
+ 'myUA',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+
+ $this->eventDispatcher->expects($this->once())
+ ->method('dispatchTyped');
+
+ $this->controller->getAppPassword();
+ }
+
+ public function testGetAppPasswordNoPassword(): void {
+ $credentials = $this->createMock(ICredentials::class);
+
+ $this->session->method('exists')
+ ->with('app_password')
+ ->willReturn(false);
+ $this->credentialStore->method('getLoginCredentials')
+ ->willReturn($credentials);
+ $credentials->method('getUid')
+ ->willReturn('myUID');
+ $credentials->method('getPassword')
+ ->willThrowException(new PasswordUnavailableException());
+ $credentials->method('getLoginName')
+ ->willReturn('myLoginName');
+ $this->request->method('getHeader')
+ ->with('user-agent')
+ ->willReturn('myUA');
+ $this->random->method('generate')
+ ->with(
+ 72,
+ ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS
+ )->willReturn('myToken');
+
+ $this->tokenProvider->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'myToken',
+ 'myUID',
+ 'myLoginName',
+ null,
+ 'myUA',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+
+ $this->eventDispatcher->expects($this->once())
+ ->method('dispatchTyped');
+
+ $this->controller->getAppPassword();
+ }
+
+ public function testDeleteAppPasswordNoAppPassword(): void {
+ $this->session->method('exists')
+ ->with('app_password')
+ ->willReturn(false);
+
+ $this->expectException(OCSForbiddenException::class);
+
+ $this->controller->deleteAppPassword();
+ }
+
+ public function testDeleteAppPasswordFails(): void {
+ $this->session->method('exists')
+ ->with('app_password')
+ ->willReturn(true);
+ $this->session->method('get')
+ ->with('app_password')
+ ->willReturn('myAppPassword');
+
+ $this->tokenProvider->method('getToken')
+ ->with('myAppPassword')
+ ->willThrowException(new InvalidTokenException());
+
+ $this->expectException(OCSForbiddenException::class);
+
+ $this->controller->deleteAppPassword();
+ }
+
+ public function testDeleteAppPasswordSuccess(): void {
+ $this->session->method('exists')
+ ->with('app_password')
+ ->willReturn(true);
+ $this->session->method('get')
+ ->with('app_password')
+ ->willReturn('myAppPassword');
+
+ $token = $this->createMock(IToken::class);
+ $this->tokenProvider->method('getToken')
+ ->with('myAppPassword')
+ ->willReturn($token);
+
+ $token->method('getUID')
+ ->willReturn('myUID');
+ $token->method('getId')
+ ->willReturn(42);
+
+ $this->tokenProvider->expects($this->once())
+ ->method('invalidateTokenById')
+ ->with(
+ 'myUID',
+ 42
+ );
+
+ $result = $this->controller->deleteAppPassword();
+
+ $this->assertEquals(new DataResponse(), $result);
+ }
+}
diff --git a/tests/Core/Controller/AutoCompleteControllerTest.php b/tests/Core/Controller/AutoCompleteControllerTest.php
new file mode 100644
index 00000000000..c5574f78fc1
--- /dev/null
+++ b/tests/Core/Controller/AutoCompleteControllerTest.php
@@ -0,0 +1,173 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Core\Controller\AutoCompleteController;
+use OCP\Collaboration\AutoComplete\IManager;
+use OCP\Collaboration\Collaborators\ISearch;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class AutoCompleteControllerTest extends TestCase {
+ /** @var ISearch|MockObject */
+ protected $collaboratorSearch;
+ /** @var IManager|MockObject */
+ protected $autoCompleteManager;
+ /** @var IEventDispatcher|MockObject */
+ protected $dispatcher;
+ /** @var AutoCompleteController */
+ protected $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ /** @var IRequest $request */
+ $request = $this->createMock(IRequest::class);
+ $this->collaboratorSearch = $this->createMock(ISearch::class);
+ $this->autoCompleteManager = $this->createMock(IManager::class);
+ $this->dispatcher = $this->createMock(IEventDispatcher::class);
+
+ $this->controller = new AutoCompleteController(
+ 'core',
+ $request,
+ $this->collaboratorSearch,
+ $this->autoCompleteManager,
+ $this->dispatcher
+ );
+ }
+
+ public static function searchDataProvider(): array {
+ return [
+ [ #0 – regular search
+ // searchResults
+ [
+ 'exact' => [
+ 'users' => [],
+ 'robots' => [],
+ ],
+ 'users' => [
+ ['label' => 'Alice A.', 'value' => ['shareWith' => 'alice']],
+ ['label' => 'Bob Y.', 'value' => ['shareWith' => 'bob']],
+ ],
+ ],
+ // expected
+ [
+ [ 'id' => 'alice', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''],
+ [ 'id' => 'bob', 'label' => 'Bob Y.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''],
+ ],
+ '',
+ 'files',
+ '42',
+ null
+ ],
+ [ #1 – missing itemtype and id
+ [
+ 'exact' => [
+ 'users' => [],
+ 'robots' => [],
+ ],
+ 'users' => [
+ ['label' => 'Alice A.', 'value' => ['shareWith' => 'alice']],
+ ['label' => 'Bob Y.', 'value' => ['shareWith' => 'bob']],
+ ],
+ ],
+ // expected
+ [
+ [ 'id' => 'alice', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''],
+ [ 'id' => 'bob', 'label' => 'Bob Y.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''],
+ ],
+ '',
+ null,
+ null,
+ null
+ ],
+ [ #2 – with sorter
+ [
+ 'exact' => [
+ 'users' => [],
+ 'robots' => [],
+ ],
+ 'users' => [
+ ['label' => 'Alice A.', 'value' => ['shareWith' => 'alice']],
+ ['label' => 'Bob Y.', 'value' => ['shareWith' => 'bob']],
+ ],
+ ],
+ // expected
+ [
+ [ 'id' => 'alice', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''],
+ [ 'id' => 'bob', 'label' => 'Bob Y.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''],
+ ],
+ '',
+ 'files',
+ '42',
+ 'karma|bus-factor'
+ ],
+ [ #3 – exact Match
+ [
+ 'exact' => [
+ 'users' => [
+ ['label' => 'Bob Y.', 'value' => ['shareWith' => 'bob']],
+ ],
+ 'robots' => [],
+ ],
+ 'users' => [
+ ['label' => 'Robert R.', 'value' => ['shareWith' => 'bobby']],
+ ],
+ ],
+ [
+ [ 'id' => 'bob', 'label' => 'Bob Y.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''],
+ [ 'id' => 'bobby', 'label' => 'Robert R.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => ''],
+ ],
+ 'bob',
+ 'files',
+ '42',
+ null
+ ],
+ [ #4 – with unique name
+ [
+ 'exact' => [
+ 'users' => [],
+ 'robots' => [],
+ ],
+ 'users' => [
+ ['label' => 'Alice A.', 'value' => ['shareWith' => 'alice'], 'shareWithDisplayNameUnique' => 'alica@nextcloud.com'],
+ ['label' => 'Alice A.', 'value' => ['shareWith' => 'alicea'], 'shareWithDisplayNameUnique' => 'alicaa@nextcloud.com'],
+ ],
+ ],
+ // expected
+ [
+ [ 'id' => 'alice', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => 'alica@nextcloud.com'],
+ [ 'id' => 'alicea', 'label' => 'Alice A.', 'icon' => '', 'source' => 'users', 'status' => '', 'subline' => '', 'shareWithDisplayNameUnique' => 'alicaa@nextcloud.com'],
+ ],
+ '',
+ 'files',
+ '42',
+ 'karma|bus-factor'
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('searchDataProvider')]
+ public function testGet(array $searchResults, array $expected, string $searchTerm, ?string $itemType, ?string $itemId, ?string $sorter): void {
+ $this->collaboratorSearch->expects($this->once())
+ ->method('search')
+ ->willReturn([$searchResults, false]);
+
+ $runSorterFrequency = $sorter === null ? $this->never() : $this->once();
+ $this->autoCompleteManager->expects($runSorterFrequency)
+ ->method('runSorters');
+
+ $response = $this->controller->get($searchTerm, $itemType, $itemId, $sorter);
+
+ $list = $response->getData();
+ $this->assertEquals($expected, $list); // has better error output…
+ $this->assertSame($expected, $list);
+ }
+}
diff --git a/tests/Core/Controller/AvatarControllerTest.php b/tests/Core/Controller/AvatarControllerTest.php
new file mode 100644
index 00000000000..a78e2c1bb5c
--- /dev/null
+++ b/tests/Core/Controller/AvatarControllerTest.php
@@ -0,0 +1,567 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OC\Core\Controller;
+
+/**
+ * Overwrite is_uploaded_file in the OC\Core\Controller namespace to allow
+ * proper unit testing of the postAvatar call.
+ */
+function is_uploaded_file($filename) {
+ return file_exists($filename);
+}
+
+namespace Tests\Core\Controller;
+
+use OC\AppFramework\Utility\TimeFactory;
+use OC\Core\Controller\AvatarController;
+use OC\Core\Controller\GuestAvatarController;
+use OCP\AppFramework\Http;
+use OCP\Files\File;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\IAvatar;
+use OCP\IAvatarManager;
+use OCP\ICache;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Class AvatarControllerTest
+ *
+ * @package OC\Core\Controller
+ */
+class AvatarControllerTest extends \Test\TestCase {
+ /** @var AvatarController */
+ private $avatarController;
+ /** @var GuestAvatarController */
+ private $guestAvatarController;
+
+ /** @var IAvatar|\PHPUnit\Framework\MockObject\MockObject */
+ private $avatarMock;
+ /** @var IUser|\PHPUnit\Framework\MockObject\MockObject */
+ private $userMock;
+ /** @var ISimpleFile|\PHPUnit\Framework\MockObject\MockObject */
+ private $avatarFile;
+ /** @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $avatarManager;
+ /** @var ICache|\PHPUnit\Framework\MockObject\MockObject */
+ private $cache;
+ /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
+ private $l;
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $userManager;
+ /** @var IRootFolder|\PHPUnit\Framework\MockObject\MockObject */
+ private $rootFolder;
+ /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $logger;
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+ /** @var TimeFactory|\PHPUnit\Framework\MockObject\MockObject */
+ private $timeFactory;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->avatarManager = $this->getMockBuilder('OCP\IAvatarManager')->getMock();
+ $this->cache = $this->getMockBuilder('OCP\ICache')
+ ->disableOriginalConstructor()->getMock();
+ $this->l = $this->getMockBuilder(IL10N::class)->getMock();
+ $this->l->method('t')->willReturnArgument(0);
+ $this->userManager = $this->getMockBuilder(IUserManager::class)->getMock();
+ $this->request = $this->getMockBuilder(IRequest::class)->getMock();
+ $this->rootFolder = $this->getMockBuilder('OCP\Files\IRootFolder')->getMock();
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
+ $this->timeFactory = $this->getMockBuilder('OC\AppFramework\Utility\TimeFactory')->getMock();
+
+ $this->avatarMock = $this->getMockBuilder('OCP\IAvatar')->getMock();
+ $this->userMock = $this->getMockBuilder(IUser::class)->getMock();
+
+ $this->guestAvatarController = new GuestAvatarController(
+ 'core',
+ $this->request,
+ $this->avatarManager,
+ $this->logger
+ );
+
+ $this->avatarController = new AvatarController(
+ 'core',
+ $this->request,
+ $this->avatarManager,
+ $this->cache,
+ $this->l,
+ $this->userManager,
+ $this->rootFolder,
+ $this->logger,
+ 'userid',
+ $this->timeFactory,
+ $this->guestAvatarController,
+ );
+
+ // Configure userMock
+ $this->userMock->method('getDisplayName')->willReturn('displayName');
+ $this->userMock->method('getUID')->willReturn('userId');
+ $this->userManager->method('get')
+ ->willReturnMap([['userId', $this->userMock]]);
+
+ $this->avatarFile = $this->getMockBuilder(ISimpleFile::class)->getMock();
+ $this->avatarFile->method('getContent')->willReturn('image data');
+ $this->avatarFile->method('getMimeType')->willReturn('image type');
+ $this->avatarFile->method('getEtag')->willReturn('my etag');
+ $this->avatarFile->method('getName')->willReturn('my name');
+ $this->avatarFile->method('getMTime')->willReturn(42);
+ }
+
+ protected function tearDown(): void {
+ parent::tearDown();
+ }
+
+ /**
+ * Fetch an avatar if a user has no avatar
+ */
+ public function testGetAvatarNoAvatar(): void {
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+ $this->avatarMock->method('getFile')->willThrowException(new NotFoundException());
+ $response = $this->avatarController->getAvatar('userId', 32);
+
+ //Comment out until JS is fixed
+ $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
+ }
+
+ /**
+ * Fetch the user's avatar
+ */
+ public function testGetAvatar(): void {
+ $this->avatarMock->method('getFile')->willReturn($this->avatarFile);
+ $this->avatarManager->method('getAvatar')->with('userId')->willReturn($this->avatarMock);
+ $this->avatarMock->expects($this->once())
+ ->method('isCustomAvatar')
+ ->willReturn(true);
+
+ $response = $this->avatarController->getAvatar('userId', 32);
+
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ $this->assertArrayHasKey('Content-Type', $response->getHeaders());
+ $this->assertEquals('image type', $response->getHeaders()['Content-Type']);
+ $this->assertArrayHasKey('X-NC-IsCustomAvatar', $response->getHeaders());
+ $this->assertEquals('1', $response->getHeaders()['X-NC-IsCustomAvatar']);
+
+ $this->assertEquals('my etag', $response->getETag());
+ }
+
+ /**
+ * Fetch the user's avatar
+ */
+ public function testGetGeneratedAvatar(): void {
+ $this->avatarMock->method('getFile')->willReturn($this->avatarFile);
+ $this->avatarManager->method('getAvatar')->with('userId')->willReturn($this->avatarMock);
+
+ $response = $this->avatarController->getAvatar('userId', 32);
+
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ $this->assertArrayHasKey('Content-Type', $response->getHeaders());
+ $this->assertEquals('image type', $response->getHeaders()['Content-Type']);
+ $this->assertArrayHasKey('X-NC-IsCustomAvatar', $response->getHeaders());
+ $this->assertEquals('0', $response->getHeaders()['X-NC-IsCustomAvatar']);
+
+ $this->assertEquals('my etag', $response->getETag());
+ }
+
+ /**
+ * Fetch the avatar of a non-existing user
+ */
+ public function testGetAvatarNoUser(): void {
+ $this->avatarManager
+ ->method('getAvatar')
+ ->with('userDoesNotExist')
+ ->willThrowException(new \Exception('user does not exist'));
+
+ $response = $this->avatarController->getAvatar('userDoesNotExist', 32);
+
+ //Comment out until JS is fixed
+ $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
+ }
+
+ public function testGetAvatarSize64(): void {
+ $this->avatarMock->expects($this->once())
+ ->method('getFile')
+ ->with($this->equalTo(64))
+ ->willReturn($this->avatarFile);
+
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+
+ $this->logger->expects($this->never())
+ ->method('debug');
+
+ $this->avatarController->getAvatar('userId', 64);
+ }
+
+ public function testGetAvatarSize512(): void {
+ $this->avatarMock->expects($this->once())
+ ->method('getFile')
+ ->with($this->equalTo(512))
+ ->willReturn($this->avatarFile);
+
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+
+ $this->logger->expects($this->never())
+ ->method('debug');
+
+ $this->avatarController->getAvatar('userId', 512);
+ }
+
+ /**
+ * Small sizes return 64 and generate a log
+ */
+ public function testGetAvatarSizeTooSmall(): void {
+ $this->avatarMock->expects($this->once())
+ ->method('getFile')
+ ->with($this->equalTo(64))
+ ->willReturn($this->avatarFile);
+
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+
+ $this->logger->expects($this->once())
+ ->method('debug')
+ ->with('Avatar requested in deprecated size 32');
+
+ $this->avatarController->getAvatar('userId', 32);
+ }
+
+ /**
+ * Avatars between 64 and 512 are upgraded to 512
+ */
+ public function testGetAvatarSizeBetween(): void {
+ $this->avatarMock->expects($this->once())
+ ->method('getFile')
+ ->with($this->equalTo(512))
+ ->willReturn($this->avatarFile);
+
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+
+ $this->logger->expects($this->once())
+ ->method('debug')
+ ->with('Avatar requested in deprecated size 65');
+
+ $this->avatarController->getAvatar('userId', 65);
+ }
+
+ /**
+ * We do not support avatars larger than 512
+ */
+ public function testGetAvatarSizeTooBig(): void {
+ $this->avatarMock->expects($this->once())
+ ->method('getFile')
+ ->with($this->equalTo(512))
+ ->willReturn($this->avatarFile);
+
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+
+ $this->logger->expects($this->once())
+ ->method('debug')
+ ->with('Avatar requested in deprecated size 513');
+
+ $this->avatarController->getAvatar('userId', 513);
+ }
+
+ /**
+ * Remove an avatar
+ */
+ public function testDeleteAvatar(): void {
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+
+ $response = $this->avatarController->deleteAvatar();
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ }
+
+ /**
+ * Test what happens if the removing of the avatar fails
+ */
+ public function testDeleteAvatarException(): void {
+ $this->avatarMock->method('remove')->willThrowException(new \Exception('foo'));
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with('foo', ['exception' => new \Exception('foo'), 'app' => 'core']);
+ $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST);
+ $this->assertEquals($expectedResponse, $this->avatarController->deleteAvatar());
+ }
+
+ /**
+ * Trying to get a tmp avatar when it is not available. 404
+ */
+ public function testTmpAvatarNoTmp(): void {
+ $response = $this->avatarController->getTmpAvatar();
+ $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus());
+ }
+
+ /**
+ * Fetch tmp avatar
+ */
+ public function testTmpAvatarValid(): void {
+ $this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
+
+ $response = $this->avatarController->getTmpAvatar();
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ }
+
+
+ /**
+ * When trying to post a new avatar a path or image should be posted.
+ */
+ public function testPostAvatarNoPathOrImage(): void {
+ $response = $this->avatarController->postAvatar(null);
+
+ $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
+ }
+
+ /**
+ * Test a correct post of an avatar using POST
+ */
+ public function testPostAvatarFile(): void {
+ //Create temp file
+ $fileName = tempnam('', 'avatarTest');
+ $copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.jpg', $fileName);
+ $this->assertTrue($copyRes);
+
+ //Create file in cache
+ $this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
+
+ //Create request return
+ $reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.jpg')]];
+ $this->request->method('getUploadedFile')->willReturn($reqRet);
+
+ $response = $this->avatarController->postAvatar(null);
+
+ //On correct upload always respond with the notsquare message
+ $this->assertEquals('notsquare', $response->getData()['data']);
+
+ //File should be deleted
+ $this->assertFalse(file_exists($fileName));
+ }
+
+ /**
+ * Test invalid post os an avatar using POST
+ */
+ public function testPostAvatarInvalidFile(): void {
+ //Create request return
+ $reqRet = ['error' => [1], 'tmp_name' => ['foo']];
+ $this->request->method('getUploadedFile')->willReturn($reqRet);
+
+ $response = $this->avatarController->postAvatar(null);
+
+ $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
+ }
+
+ /**
+ * Check what happens when we upload a GIF
+ */
+ public function testPostAvatarFileGif(): void {
+ //Create temp file
+ $fileName = tempnam('', 'avatarTest');
+ $copyRes = copy(\OC::$SERVERROOT . '/tests/data/testimage.gif', $fileName);
+ $this->assertTrue($copyRes);
+
+ //Create file in cache
+ $this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.gif'));
+
+ //Create request return
+ $reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [filesize(\OC::$SERVERROOT . '/tests/data/testimage.gif')]];
+ $this->request->method('getUploadedFile')->willReturn($reqRet);
+
+ $response = $this->avatarController->postAvatar(null);
+
+ $this->assertEquals('Unknown filetype', $response->getData()['data']['message']);
+
+ //File should be deleted
+ $this->assertFalse(file_exists($fileName));
+ }
+
+ /**
+ * Test posting avatar from existing file
+ */
+ public function testPostAvatarFromFile(): void {
+ //Mock node API call
+ $file = $this->getMockBuilder('OCP\Files\File')
+ ->disableOriginalConstructor()->getMock();
+ $file->expects($this->once())
+ ->method('getContent')
+ ->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
+ $file->expects($this->once())
+ ->method('getMimeType')
+ ->willReturn('image/jpeg');
+ $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
+ $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder);
+ $userFolder->method('get')->willReturn($file);
+
+ //Create request return
+ $response = $this->avatarController->postAvatar('avatar.jpg');
+
+ //On correct upload always respond with the notsquare message
+ $this->assertEquals('notsquare', $response->getData()['data']);
+ }
+
+ /**
+ * Test posting avatar from existing folder
+ */
+ public function testPostAvatarFromNoFile(): void {
+ $file = $this->getMockBuilder('OCP\Files\Node')->getMock();
+ $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
+ $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder);
+ $userFolder
+ ->method('get')
+ ->with('folder')
+ ->willReturn($file);
+
+ //Create request return
+ $response = $this->avatarController->postAvatar('folder');
+
+ //On correct upload always respond with the notsquare message
+ $this->assertEquals(['data' => ['message' => 'Please select a file.']], $response->getData());
+ }
+
+ public function testPostAvatarInvalidType(): void {
+ $file = $this->getMockBuilder('OCP\Files\File')
+ ->disableOriginalConstructor()->getMock();
+ $file->expects($this->never())
+ ->method('getContent');
+ $file->expects($this->exactly(2))
+ ->method('getMimeType')
+ ->willReturn('text/plain');
+ $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
+ $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder);
+ $userFolder->method('get')->willReturn($file);
+
+ $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'The selected file is not an image.']], Http::STATUS_BAD_REQUEST);
+ $this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg'));
+ }
+
+ public function testPostAvatarNotPermittedException(): void {
+ $file = $this->getMockBuilder('OCP\Files\File')
+ ->disableOriginalConstructor()->getMock();
+ $file->expects($this->once())
+ ->method('getContent')
+ ->willThrowException(new NotPermittedException());
+ $file->expects($this->once())
+ ->method('getMimeType')
+ ->willReturn('image/jpeg');
+ $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
+ $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder);
+ $userFolder->method('get')->willReturn($file);
+
+ $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'The selected file cannot be read.']], Http::STATUS_BAD_REQUEST);
+ $this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg'));
+ }
+
+ /**
+ * Test what happens if the upload of the avatar fails
+ */
+ public function testPostAvatarException(): void {
+ $this->cache->expects($this->once())
+ ->method('set')
+ ->willThrowException(new \Exception('foo'));
+ $file = $this->getMockBuilder('OCP\Files\File')
+ ->disableOriginalConstructor()->getMock();
+ $file->expects($this->once())
+ ->method('getContent')
+ ->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
+ $file->expects($this->once())
+ ->method('getMimeType')
+ ->willReturn('image/jpeg');
+ $userFolder = $this->getMockBuilder('OCP\Files\Folder')->getMock();
+ $this->rootFolder->method('getUserFolder')->with('userid')->willReturn($userFolder);
+ $userFolder->method('get')->willReturn($file);
+
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with('foo', ['exception' => new \Exception('foo'), 'app' => 'core']);
+ $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_OK);
+ $this->assertEquals($expectedResponse, $this->avatarController->postAvatar('avatar.jpg'));
+ }
+
+
+ /**
+ * Test invalid crop argument
+ */
+ public function testPostCroppedAvatarInvalidCrop(): void {
+ $response = $this->avatarController->postCroppedAvatar([]);
+
+ $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
+ }
+
+ /**
+ * Test no tmp avatar to crop
+ */
+ public function testPostCroppedAvatarNoTmpAvatar(): void {
+ $response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 10]);
+
+ $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
+ }
+
+ /**
+ * Test with non square crop
+ */
+ public function testPostCroppedAvatarNoSquareCrop(): void {
+ $this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
+
+ $this->avatarMock->method('set')->willThrowException(new \OC\NotSquareException);
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+ $response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 11]);
+
+ $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus());
+ }
+
+ /**
+ * Check for proper reply on proper crop argument
+ */
+ public function testPostCroppedAvatarValidCrop(): void {
+ $this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+ $response = $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 10]);
+
+ $this->assertEquals(Http::STATUS_OK, $response->getStatus());
+ $this->assertEquals('success', $response->getData()['status']);
+ }
+
+ /**
+ * Test what happens if the cropping of the avatar fails
+ */
+ public function testPostCroppedAvatarException(): void {
+ $this->cache->method('get')->willReturn(file_get_contents(\OC::$SERVERROOT . '/tests/data/testimage.jpg'));
+
+ $this->avatarMock->method('set')->willThrowException(new \Exception('foo'));
+ $this->avatarManager->method('getAvatar')->willReturn($this->avatarMock);
+
+ $this->logger->expects($this->once())
+ ->method('error')
+ ->with('foo', ['exception' => new \Exception('foo'), 'app' => 'core']);
+ $expectedResponse = new Http\JSONResponse(['data' => ['message' => 'An error occurred. Please contact your admin.']], Http::STATUS_BAD_REQUEST);
+ $this->assertEquals($expectedResponse, $this->avatarController->postCroppedAvatar(['x' => 0, 'y' => 0, 'w' => 10, 'h' => 11]));
+ }
+
+
+ /**
+ * Check for proper reply on proper crop argument
+ */
+ public function testFileTooBig(): void {
+ $fileName = \OC::$SERVERROOT . '/tests/data/testimage.jpg';
+ //Create request return
+ $reqRet = ['error' => [0], 'tmp_name' => [$fileName], 'size' => [21 * 1024 * 1024]];
+ $this->request->method('getUploadedFile')->willReturn($reqRet);
+
+ $response = $this->avatarController->postAvatar(null);
+
+ $this->assertEquals('File is too big', $response->getData()['data']['message']);
+ }
+}
diff --git a/tests/Core/Controller/CSRFTokenControllerTest.php b/tests/Core/Controller/CSRFTokenControllerTest.php
new file mode 100644
index 00000000000..a401788be8d
--- /dev/null
+++ b/tests/Core/Controller/CSRFTokenControllerTest.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Core\Controller\CSRFTokenController;
+use OC\Security\CSRF\CsrfToken;
+use OC\Security\CSRF\CsrfTokenManager;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use Test\TestCase;
+
+class CSRFTokenControllerTest extends TestCase {
+ /** @var CSRFTokenController */
+ private $controller;
+
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+
+ /** @var CsrfTokenManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $tokenManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->tokenManager = $this->createMock(CsrfTokenManager::class);
+
+ $this->controller = new CSRFTokenController('core', $this->request,
+ $this->tokenManager);
+ }
+
+ public function testGetToken(): void {
+ $this->request->method('passesStrictCookieCheck')->willReturn(true);
+
+ $token = $this->createMock(CsrfToken::class);
+ $this->tokenManager->method('getToken')->willReturn($token);
+ $token->method('getEncryptedValue')->willReturn('toktok123');
+
+ $response = $this->controller->index();
+
+ $this->assertInstanceOf(JSONResponse::class, $response);
+ $this->assertSame(Http::STATUS_OK, $response->getStatus());
+ $this->assertEquals([
+ 'token' => 'toktok123'
+ ], $response->getData());
+ }
+
+ public function testGetTokenNoStrictSameSiteCookie(): void {
+ $this->request->method('passesStrictCookieCheck')->willReturn(false);
+
+ $response = $this->controller->index();
+
+ $this->assertInstanceOf(JSONResponse::class, $response);
+ $this->assertSame(Http::STATUS_FORBIDDEN, $response->getStatus());
+ }
+}
diff --git a/tests/Core/Controller/ChangePasswordControllerTest.php b/tests/Core/Controller/ChangePasswordControllerTest.php
new file mode 100644
index 00000000000..aae36fb52b8
--- /dev/null
+++ b/tests/Core/Controller/ChangePasswordControllerTest.php
@@ -0,0 +1,193 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\User\Session;
+use OCA\Settings\Controller\ChangePasswordController;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\HintException;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserManager;
+
+class ChangePasswordControllerTest extends \Test\TestCase {
+ /** @var string */
+ private $userId = 'currentUser';
+ /** @var string */
+ private $loginName = 'ua1337';
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $userManager;
+ /** @var Session|\PHPUnit\Framework\MockObject\MockObject */
+ private $userSession;
+ /** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $groupManager;
+ /** @var IAppManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $appManager;
+ /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */
+ private $l;
+ /** @var ChangePasswordController */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(\OC\User\Manager::class);
+ $this->userSession = $this->createMock(Session::class);
+ $this->groupManager = $this->createMock(\OC\Group\Manager::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+ $this->l = $this->createMock(IL10N::class);
+ $this->l->method('t')->willReturnArgument(0);
+
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject $request */
+ $request = $this->createMock(IRequest::class);
+
+ $this->controller = new ChangePasswordController(
+ 'core',
+ $request,
+ $this->userId,
+ $this->userManager,
+ $this->userSession,
+ $this->groupManager,
+ $this->appManager,
+ $this->l
+ );
+ }
+
+ public function testChangePersonalPasswordWrongPassword(): void {
+ $this->userSession->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn($this->loginName);
+
+ $this->userManager->expects($this->once())
+ ->method('checkPassword')
+ ->with($this->loginName, 'old')
+ ->willReturn(false);
+
+ $expects = new JSONResponse([
+ 'status' => 'error',
+ 'data' => [
+ 'message' => 'Wrong password',
+ ],
+ ]);
+ $expects->throttle();
+
+ $actual = $this->controller->changePersonalPassword('old', 'new');
+ $this->assertEquals($expects, $actual);
+ }
+
+ public function testChangePersonalPasswordCommonPassword(): void {
+ $this->userSession->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn($this->loginName);
+
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $this->userManager->expects($this->once())
+ ->method('checkPassword')
+ ->with($this->loginName, 'old')
+ ->willReturn($user);
+
+ $user->expects($this->once())
+ ->method('setPassword')
+ ->with('new')
+ ->willThrowException(new HintException('Common password'));
+
+ $expects = new JSONResponse([
+ 'status' => 'error',
+ 'data' => [
+ 'message' => 'Common password',
+ ],
+ ]);
+
+ $actual = $this->controller->changePersonalPassword('old', 'new');
+ $this->assertEquals($expects, $actual);
+ }
+
+ public function testChangePersonalPasswordNoNewPassword(): void {
+ $this->userSession->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn($this->loginName);
+
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $this->userManager->expects($this->once())
+ ->method('checkPassword')
+ ->with($this->loginName, 'old')
+ ->willReturn($user);
+
+ $expects = [
+ 'status' => 'error',
+ 'data' => [
+ 'message' => 'Unable to change personal password',
+ ],
+ ];
+
+ $res = $this->controller->changePersonalPassword('old');
+
+ $this->assertEquals($expects, $res->getData());
+ }
+
+ public function testChangePersonalPasswordCantSetPassword(): void {
+ $this->userSession->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn($this->loginName);
+
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $this->userManager->expects($this->once())
+ ->method('checkPassword')
+ ->with($this->loginName, 'old')
+ ->willReturn($user);
+
+ $user->expects($this->once())
+ ->method('setPassword')
+ ->with('new')
+ ->willReturn(false);
+
+ $expects = new JSONResponse([
+ 'status' => 'error',
+ 'data' => [
+ 'message' => 'Unable to change personal password',
+ ],
+ ]);
+
+ $actual = $this->controller->changePersonalPassword('old', 'new');
+ $this->assertEquals($expects, $actual);
+ }
+
+ public function testChangePersonalPassword(): void {
+ $this->userSession->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn($this->loginName);
+
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $this->userManager->expects($this->once())
+ ->method('checkPassword')
+ ->with($this->loginName, 'old')
+ ->willReturn($user);
+
+ $user->expects($this->once())
+ ->method('setPassword')
+ ->with('new')
+ ->willReturn(true);
+
+ $this->userSession->expects($this->once())
+ ->method('updateSessionTokenPassword')
+ ->with('new');
+
+ $expects = new JSONResponse([
+ 'status' => 'success',
+ 'data' => [
+ 'message' => 'Saved',
+ ],
+ ]);
+
+ $actual = $this->controller->changePersonalPassword('old', 'new');
+ $this->assertEquals($expects, $actual);
+ }
+}
diff --git a/tests/Core/Controller/ClientFlowLoginControllerTest.php b/tests/Core/Controller/ClientFlowLoginControllerTest.php
new file mode 100644
index 00000000000..b182bb1bb39
--- /dev/null
+++ b/tests/Core/Controller/ClientFlowLoginControllerTest.php
@@ -0,0 +1,685 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Exceptions\PasswordlessTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OC\Core\Controller\ClientFlowLoginController;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use OCA\OAuth2\Db\Client;
+use OCA\OAuth2\Db\ClientMapper;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\ContentSecurityPolicy;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\AppFramework\Http\Response;
+use OCP\AppFramework\Http\StandaloneTemplateResponse;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Defaults;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Security\ICrypto;
+use OCP\Security\ISecureRandom;
+use OCP\Session\Exceptions\SessionNotAvailableException;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class ClientFlowLoginControllerTest extends TestCase {
+ private IRequest&MockObject $request;
+ private IUserSession&MockObject $userSession;
+ private IL10N&MockObject $l10n;
+ private Defaults&MockObject $defaults;
+ private ISession&MockObject $session;
+ private IProvider&MockObject $tokenProvider;
+ private ISecureRandom&MockObject $random;
+ private IURLGenerator&MockObject $urlGenerator;
+ private ClientMapper&MockObject $clientMapper;
+ private AccessTokenMapper&MockObject $accessTokenMapper;
+ private ICrypto&MockObject $crypto;
+ private IEventDispatcher&MockObject $eventDispatcher;
+ private ITimeFactory&MockObject $timeFactory;
+ private IConfig&MockObject $config;
+
+ private ClientFlowLoginController $clientFlowLoginController;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->l10n
+ ->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($text, $parameters = []) {
+ return vsprintf($text, $parameters);
+ });
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->tokenProvider = $this->createMock(IProvider::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->clientMapper = $this->createMock(ClientMapper::class);
+ $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class);
+ $this->crypto = $this->createMock(ICrypto::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->config = $this->createMock(IConfig::class);
+
+ $this->clientFlowLoginController = new ClientFlowLoginController(
+ 'core',
+ $this->request,
+ $this->userSession,
+ $this->l10n,
+ $this->defaults,
+ $this->session,
+ $this->tokenProvider,
+ $this->random,
+ $this->urlGenerator,
+ $this->clientMapper,
+ $this->accessTokenMapper,
+ $this->crypto,
+ $this->eventDispatcher,
+ $this->timeFactory,
+ $this->config,
+ );
+ }
+
+ public function testShowAuthPickerPageNoClientOrOauthRequest(): void {
+ $expected = new StandaloneTemplateResponse(
+ 'core',
+ 'error',
+ [
+ 'errors'
+ => [
+ [
+ 'error' => 'Access Forbidden',
+ 'hint' => 'Invalid request',
+ ],
+ ],
+ ],
+ 'guest'
+ );
+
+ $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
+ }
+
+ public function testShowAuthPickerPageWithOcsHeader(): void {
+ $this->request
+ ->method('getHeader')
+ ->willReturnMap([
+ ['user-agent', 'Mac OS X Sync Client'],
+ ['OCS-APIREQUEST', 'true'],
+ ]);
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(
+ 64,
+ ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS
+ )
+ ->willReturn('StateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('set')
+ ->with('client.flow.state.token', 'StateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('oauth.state')
+ ->willReturn('OauthStateToken');
+ $this->defaults
+ ->expects($this->once())
+ ->method('getName')
+ ->willReturn('ExampleCloud');
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+ $this->request
+ ->method('getServerProtocol')
+ ->willReturn('https');
+
+ $expected = new StandaloneTemplateResponse(
+ 'core',
+ 'loginflow/authpicker',
+ [
+ 'client' => 'Mac OS X Sync Client',
+ 'clientIdentifier' => '',
+ 'instanceName' => 'ExampleCloud',
+ 'urlGenerator' => $this->urlGenerator,
+ 'stateToken' => 'StateToken',
+ 'serverHost' => 'https://example.com',
+ 'oauthState' => 'OauthStateToken',
+ 'user' => '',
+ 'direct' => 0,
+ 'providedRedirectUri' => '',
+ ],
+ 'guest'
+ );
+ $csp = new ContentSecurityPolicy();
+ $csp->addAllowedFormActionDomain('nc://*');
+ $expected->setContentSecurityPolicy($csp);
+ $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
+ }
+
+ public function testShowAuthPickerPageWithOauth(): void {
+ $this->request
+ ->method('getHeader')
+ ->willReturnMap([
+ ['user-agent', 'Mac OS X Sync Client'],
+ ['OCS-APIREQUEST', 'false'],
+ ]);
+ $client = new Client();
+ $client->setName('My external service');
+ $client->setRedirectUri('https://example.com/redirect.php');
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('getByIdentifier')
+ ->with('MyClientIdentifier')
+ ->willReturn($client);
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(
+ 64,
+ ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS
+ )
+ ->willReturn('StateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('set')
+ ->with('client.flow.state.token', 'StateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('oauth.state')
+ ->willReturn('OauthStateToken');
+ $this->defaults
+ ->expects($this->once())
+ ->method('getName')
+ ->willReturn('ExampleCloud');
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+ $this->request
+ ->method('getServerProtocol')
+ ->willReturn('https');
+
+ $expected = new StandaloneTemplateResponse(
+ 'core',
+ 'loginflow/authpicker',
+ [
+ 'client' => 'My external service',
+ 'clientIdentifier' => 'MyClientIdentifier',
+ 'instanceName' => 'ExampleCloud',
+ 'urlGenerator' => $this->urlGenerator,
+ 'stateToken' => 'StateToken',
+ 'serverHost' => 'https://example.com',
+ 'oauthState' => 'OauthStateToken',
+ 'user' => '',
+ 'direct' => 0,
+ 'providedRedirectUri' => '',
+ ],
+ 'guest'
+ );
+ $csp = new ContentSecurityPolicy();
+ $csp->addAllowedFormActionDomain('https://example.com/redirect.php');
+ $expected->setContentSecurityPolicy($csp);
+ $this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage('MyClientIdentifier'));
+ }
+
+ public function testGenerateAppPasswordWithInvalidToken(): void {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('OtherToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+
+ $expected = new StandaloneTemplateResponse(
+ 'core',
+ '403',
+ [
+ 'message' => 'State token does not match',
+ ],
+ 'guest'
+ );
+ $expected->setStatus(Http::STATUS_FORBIDDEN);
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ public function testGenerateAppPasswordWithSessionNotAvailableException(): void {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willThrowException(new SessionNotAvailableException());
+
+ $expected = new Response();
+ $expected->setStatus(Http::STATUS_FORBIDDEN);
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ public function testGenerateAppPasswordWithInvalidTokenException(): void {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willThrowException(new InvalidTokenException());
+
+ $expected = new Response();
+ $expected->setStatus(Http::STATUS_FORBIDDEN);
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ public function testGeneratePasswordWithPassword(): void {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $myToken = $this->createMock(IToken::class);
+ $myToken
+ ->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn('MyLoginName');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willReturn($myToken);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getPassword')
+ ->with($myToken, 'SessionId')
+ ->willReturn('MyPassword');
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(72)
+ ->willReturn('MyGeneratedToken');
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('MyUid');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'MyGeneratedToken',
+ 'MyUid',
+ 'MyLoginName',
+ 'MyPassword',
+ 'unknown',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+ $this->request
+ ->expects($this->once())
+ ->method('getServerProtocol')
+ ->willReturn('http');
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+ $this->request
+ ->expects($this->any())
+ ->method('getHeader')
+ ->willReturn('');
+
+ $this->eventDispatcher->expects($this->once())
+ ->method('dispatchTyped');
+
+ $expected = new RedirectResponse('nc://login/server:http://example.com&user:MyLoginName&password:MyGeneratedToken');
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ /**
+ * @param string $redirectUri
+ * @param string $redirectUrl
+ *
+ * @testWith
+ * ["https://example.com/redirect.php", "https://example.com/redirect.php?state=MyOauthState&code=MyAccessCode"]
+ * ["https://example.com/redirect.php?hello=world", "https://example.com/redirect.php?hello=world&state=MyOauthState&code=MyAccessCode"]
+ *
+ */
+ public function testGeneratePasswordWithPasswordForOauthClient($redirectUri, $redirectUrl): void {
+ $this->session
+ ->method('get')
+ ->willReturnMap([
+ ['client.flow.state.token', 'MyStateToken'],
+ ['oauth.state', 'MyOauthState'],
+ ]);
+ $calls = [
+ 'client.flow.state.token',
+ 'oauth.state',
+ ];
+ $this->session
+ ->method('remove')
+ ->willReturnCallback(function ($key) use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, $key);
+ });
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $myToken = $this->createMock(IToken::class);
+ $myToken
+ ->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn('MyLoginName');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willReturn($myToken);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getPassword')
+ ->with($myToken, 'SessionId')
+ ->willReturn('MyPassword');
+ $this->random
+ ->method('generate')
+ ->willReturnMap([
+ [72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS, 'MyGeneratedToken'],
+ [128, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS, 'MyAccessCode'],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('MyUid');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $token = $this->createMock(IToken::class);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'MyGeneratedToken',
+ 'MyUid',
+ 'MyLoginName',
+ 'MyPassword',
+ 'My OAuth client',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ )
+ ->willReturn($token);
+ $client = new Client();
+ $client->setName('My OAuth client');
+ $client->setRedirectUri($redirectUri);
+ $this->clientMapper
+ ->expects($this->once())
+ ->method('getByIdentifier')
+ ->with('MyClientIdentifier')
+ ->willReturn($client);
+
+ $this->eventDispatcher->expects($this->once())
+ ->method('dispatchTyped');
+
+ $expected = new RedirectResponse($redirectUrl);
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken', 'MyClientIdentifier'));
+ }
+
+ public function testGeneratePasswordWithoutPassword(): void {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $myToken = $this->createMock(IToken::class);
+ $myToken
+ ->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn('MyLoginName');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willReturn($myToken);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getPassword')
+ ->with($myToken, 'SessionId')
+ ->willThrowException(new PasswordlessTokenException());
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(72)
+ ->willReturn('MyGeneratedToken');
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('MyUid');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'MyGeneratedToken',
+ 'MyUid',
+ 'MyLoginName',
+ null,
+ 'unknown',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+ $this->request
+ ->expects($this->once())
+ ->method('getServerProtocol')
+ ->willReturn('http');
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+ $this->request
+ ->expects($this->any())
+ ->method('getHeader')
+ ->willReturn('');
+
+ $this->eventDispatcher->expects($this->once())
+ ->method('dispatchTyped');
+
+ $expected = new RedirectResponse('nc://login/server:http://example.com&user:MyLoginName&password:MyGeneratedToken');
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+
+ public static function dataGeneratePasswordWithHttpsProxy(): array {
+ return [
+ [
+ [
+ ['X-Forwarded-Proto', 'http'],
+ ['X-Forwarded-Ssl', 'off'],
+ ['user-agent', ''],
+ ],
+ 'http',
+ 'http',
+ ],
+ [
+ [
+ ['X-Forwarded-Proto', 'http'],
+ ['X-Forwarded-Ssl', 'off'],
+ ['user-agent', ''],
+ ],
+ 'https',
+ 'https',
+ ],
+ [
+ [
+ ['X-Forwarded-Proto', 'https'],
+ ['X-Forwarded-Ssl', 'off'],
+ ['user-agent', ''],
+ ],
+ 'http',
+ 'https',
+ ],
+ [
+ [
+ ['X-Forwarded-Proto', 'https'],
+ ['X-Forwarded-Ssl', 'on'],
+ ['user-agent', ''],
+ ],
+ 'http',
+ 'https',
+ ],
+ [
+ [
+ ['X-Forwarded-Proto', 'http'],
+ ['X-Forwarded-Ssl', 'on'],
+ ['user-agent', ''],
+ ],
+ 'http',
+ 'https',
+ ],
+ ];
+ }
+
+ /**
+ * @param array $headers
+ * @param string $protocol
+ * @param string $expected
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGeneratePasswordWithHttpsProxy')]
+ public function testGeneratePasswordWithHttpsProxy(array $headers, $protocol, $expected): void {
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('client.flow.state.token')
+ ->willReturn('MyStateToken');
+ $this->session
+ ->expects($this->once())
+ ->method('remove')
+ ->with('client.flow.state.token');
+ $this->session
+ ->expects($this->once())
+ ->method('getId')
+ ->willReturn('SessionId');
+ $myToken = $this->createMock(IToken::class);
+ $myToken
+ ->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn('MyLoginName');
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getToken')
+ ->with('SessionId')
+ ->willReturn($myToken);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('getPassword')
+ ->with($myToken, 'SessionId')
+ ->willReturn('MyPassword');
+ $this->random
+ ->expects($this->once())
+ ->method('generate')
+ ->with(72)
+ ->willReturn('MyGeneratedToken');
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('MyUid');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->tokenProvider
+ ->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'MyGeneratedToken',
+ 'MyUid',
+ 'MyLoginName',
+ 'MyPassword',
+ 'unknown',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+ $this->request
+ ->expects($this->once())
+ ->method('getServerProtocol')
+ ->willReturn($protocol);
+ $this->request
+ ->expects($this->once())
+ ->method('getServerHost')
+ ->willReturn('example.com');
+ $this->request
+ ->expects($this->atLeastOnce())
+ ->method('getHeader')
+ ->willReturnMap($headers);
+
+ $this->eventDispatcher->expects($this->once())
+ ->method('dispatchTyped');
+
+ $expected = new RedirectResponse('nc://login/server:' . $expected . '://example.com&user:MyLoginName&password:MyGeneratedToken');
+ $this->assertEquals($expected, $this->clientFlowLoginController->generateAppPassword('MyStateToken'));
+ }
+}
diff --git a/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php b/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php
new file mode 100644
index 00000000000..d130eb75c1a
--- /dev/null
+++ b/tests/Core/Controller/ClientFlowLoginV2ControllerTest.php
@@ -0,0 +1,394 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Controller;
+
+use OC\Core\Controller\ClientFlowLoginV2Controller;
+use OC\Core\Data\LoginFlowV2Credentials;
+use OC\Core\Db\LoginFlowV2;
+use OC\Core\Exception\LoginFlowV2ClientForbiddenException;
+use OC\Core\Exception\LoginFlowV2NotFoundException;
+use OC\Core\Service\LoginFlowV2Service;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\AppFramework\Http\StandaloneTemplateResponse;
+use OCP\Defaults;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class ClientFlowLoginV2ControllerTest extends TestCase {
+ /** @var IRequest|MockObject */
+ private $request;
+ /** @var LoginFlowV2Service|MockObject */
+ private $loginFlowV2Service;
+ /** @var IURLGenerator|MockObject */
+ private $urlGenerator;
+ /** @var ISession|MockObject */
+ private $session;
+ /** @var IUserSession|MockObject */
+ private $userSession;
+ /** @var ISecureRandom|MockObject */
+ private $random;
+ /** @var Defaults|MockObject */
+ private $defaults;
+ /** @var IL10N|MockObject */
+ private $l;
+ /** @var ClientFlowLoginV2Controller */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->loginFlowV2Service = $this->createMock(LoginFlowV2Service::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->random = $this->createMock(ISecureRandom::class);
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->l = $this->createMock(IL10N::class);
+ $this->l
+ ->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($text, $parameters = []) {
+ return vsprintf($text, $parameters);
+ });
+ $this->controller = new ClientFlowLoginV2Controller(
+ 'core',
+ $this->request,
+ $this->loginFlowV2Service,
+ $this->urlGenerator,
+ $this->session,
+ $this->userSession,
+ $this->random,
+ $this->defaults,
+ 'user',
+ $this->l
+ );
+ }
+
+ public function testPollInvalid(): void {
+ $this->loginFlowV2Service->method('poll')
+ ->with('token')
+ ->willThrowException(new LoginFlowV2NotFoundException());
+
+ $result = $this->controller->poll('token');
+
+ $this->assertSame([], $result->getData());
+ $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus());
+ }
+
+ public function testPollValid(): void {
+ $creds = new LoginFlowV2Credentials('server', 'login', 'pass');
+ $this->loginFlowV2Service->method('poll')
+ ->with('token')
+ ->willReturn($creds);
+
+ $result = $this->controller->poll('token');
+
+ $this->assertSame($creds->jsonSerialize(), $result->getData());
+ $this->assertSame(Http::STATUS_OK, $result->getStatus());
+ }
+
+ public function testLandingInvalid(): void {
+ $this->session->expects($this->never())
+ ->method($this->anything());
+
+ $this->loginFlowV2Service->method('startLoginFlow')
+ ->with('token')
+ ->willReturn(false);
+
+ $result = $this->controller->landing('token');
+
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ $this->assertInstanceOf(StandaloneTemplateResponse::class, $result);
+ }
+
+ public function testLandingValid(): void {
+ $this->session->expects($this->once())
+ ->method('set')
+ ->with('client.flow.v2.login.token', 'token');
+
+ $this->loginFlowV2Service->method('startLoginFlow')
+ ->with('token')
+ ->willReturn(true);
+
+ $this->urlGenerator->method('linkToRouteAbsolute')
+ ->with('core.ClientFlowLoginV2.showAuthPickerPage')
+ ->willReturn('https://server/path');
+
+ $result = $this->controller->landing('token');
+
+ $this->assertInstanceOf(RedirectResponse::class, $result);
+ $this->assertSame(Http::STATUS_SEE_OTHER, $result->getStatus());
+ $this->assertSame('https://server/path', $result->getRedirectURL());
+ }
+
+ public function testShowAuthPickerNoLoginToken(): void {
+ $this->session->method('get')
+ ->willReturn(null);
+
+ $result = $this->controller->showAuthPickerPage();
+
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ }
+
+ public function testShowAuthPickerInvalidLoginToken(): void {
+ $this->session->method('get')
+ ->with('client.flow.v2.login.token')
+ ->willReturn('loginToken');
+
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willThrowException(new LoginFlowV2NotFoundException());
+
+ $result = $this->controller->showAuthPickerPage();
+
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ }
+
+ public function testShowAuthPickerForbiddenUserClient() {
+ $this->session->method('get')
+ ->with('client.flow.v2.login.token')
+ ->willReturn('loginToken');
+
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willThrowException(new LoginFlowV2ClientForbiddenException());
+
+ $result = $this->controller->showAuthPickerPage();
+
+ $this->assertInstanceOf(StandaloneTemplateResponse::class, $result);
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ $this->assertSame('Please use original client', $result->getParams()['message']);
+ }
+
+ public function testShowAuthPickerValidLoginToken(): void {
+ $this->session->method('get')
+ ->with('client.flow.v2.login.token')
+ ->willReturn('loginToken');
+
+ $flow = new LoginFlowV2();
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willReturn($flow);
+
+ $this->random->method('generate')
+ ->with(64, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS)
+ ->willReturn('random');
+ $this->session->expects($this->once())
+ ->method('set')
+ ->with('client.flow.v2.state.token', 'random');
+
+ $this->controller->showAuthPickerPage();
+ }
+
+ public function testGrantPageNoStateToken(): void {
+ $result = $this->controller->grantPage(null);
+
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ }
+
+ public function testGrantPageInvalidStateToken(): void {
+ $this->session->method('get')
+ ->willReturnCallback(function ($name) {
+ return null;
+ });
+
+ $result = $this->controller->grantPage('stateToken');
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ }
+
+ public function testGrantPageInvalidLoginToken(): void {
+ $this->session->method('get')
+ ->willReturnCallback(function ($name) {
+ if ($name === 'client.flow.v2.state.token') {
+ return 'stateToken';
+ }
+ if ($name === 'client.flow.v2.login.token') {
+ return 'loginToken';
+ }
+ return null;
+ });
+
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willThrowException(new LoginFlowV2NotFoundException());
+
+ $result = $this->controller->grantPage('stateToken');
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ }
+
+ public function testGrantPageForbiddenUserClient() {
+ $this->session->method('get')
+ ->willReturnCallback(function ($name) {
+ if ($name === 'client.flow.v2.state.token') {
+ return 'stateToken';
+ }
+ if ($name === 'client.flow.v2.login.token') {
+ return 'loginToken';
+ }
+ return null;
+ });
+
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willThrowException(new LoginFlowV2ClientForbiddenException());
+
+ $result = $this->controller->grantPage('stateToken');
+
+ $this->assertInstanceOf(StandaloneTemplateResponse::class, $result);
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ $this->assertSame('Please use original client', $result->getParams()['message']);
+ }
+
+ public function testGrantPageValid(): void {
+ $this->session->method('get')
+ ->willReturnCallback(function ($name) {
+ if ($name === 'client.flow.v2.state.token') {
+ return 'stateToken';
+ }
+ if ($name === 'client.flow.v2.login.token') {
+ return 'loginToken';
+ }
+ return null;
+ });
+
+ $user = $this->createMock(IUser::class);
+ $user->method('getUID')
+ ->willReturn('uid');
+ $user->method('getDisplayName')
+ ->willReturn('display name');
+ $this->userSession->method('getUser')
+ ->willReturn($user);
+
+ $flow = new LoginFlowV2();
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willReturn($flow);
+
+ $result = $this->controller->grantPage('stateToken');
+ $this->assertSame(Http::STATUS_OK, $result->getStatus());
+ }
+
+
+ public function testGenerateAppPasswordInvalidStateToken(): void {
+ $this->session->method('get')
+ ->willReturnCallback(function ($name) {
+ return null;
+ });
+
+ $result = $this->controller->generateAppPassword('stateToken');
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ }
+
+ public function testGenerateAppPassworInvalidLoginToken(): void {
+ $this->session->method('get')
+ ->willReturnCallback(function ($name) {
+ if ($name === 'client.flow.v2.state.token') {
+ return 'stateToken';
+ }
+ if ($name === 'client.flow.v2.login.token') {
+ return 'loginToken';
+ }
+ return null;
+ });
+
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willThrowException(new LoginFlowV2NotFoundException());
+
+ $result = $this->controller->generateAppPassword('stateToken');
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ }
+
+ public function testGenerateAppPasswordForbiddenUserClient() {
+ $this->session->method('get')
+ ->willReturnCallback(function ($name) {
+ if ($name === 'client.flow.v2.state.token') {
+ return 'stateToken';
+ }
+ if ($name === 'client.flow.v2.login.token') {
+ return 'loginToken';
+ }
+ return null;
+ });
+
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willThrowException(new LoginFlowV2ClientForbiddenException());
+
+ $result = $this->controller->generateAppPassword('stateToken');
+
+ $this->assertInstanceOf(StandaloneTemplateResponse::class, $result);
+ $this->assertSame(Http::STATUS_FORBIDDEN, $result->getStatus());
+ $this->assertSame('Please use original client', $result->getParams()['message']);
+ }
+
+ public function testGenerateAppPassworValid(): void {
+ $this->session->method('get')
+ ->willReturnCallback(function ($name) {
+ if ($name === 'client.flow.v2.state.token') {
+ return 'stateToken';
+ }
+ if ($name === 'client.flow.v2.login.token') {
+ return 'loginToken';
+ }
+ return null;
+ });
+
+ $flow = new LoginFlowV2();
+ $this->loginFlowV2Service->method('getByLoginToken')
+ ->with('loginToken')
+ ->willReturn($flow);
+
+ $clearedState = false;
+ $clearedLogin = false;
+ $this->session->method('remove')
+ ->willReturnCallback(function ($name) use (&$clearedLogin, &$clearedState): void {
+ if ($name === 'client.flow.v2.state.token') {
+ $clearedState = true;
+ }
+ if ($name === 'client.flow.v2.login.token') {
+ $clearedLogin = true;
+ }
+ });
+
+ $this->session->method('getId')
+ ->willReturn('sessionId');
+
+ $this->loginFlowV2Service->expects($this->once())
+ ->method('flowDone')
+ ->with(
+ 'loginToken',
+ 'sessionId',
+ 'https://server',
+ 'user'
+ )->willReturn(true);
+
+ $this->request->method('getServerProtocol')
+ ->willReturn('https');
+ $this->request->method('getRequestUri')
+ ->willReturn('/login/v2');
+ $this->request->method('getServerHost')
+ ->willReturn('server');
+
+ $result = $this->controller->generateAppPassword('stateToken');
+ $this->assertSame(Http::STATUS_OK, $result->getStatus());
+
+ $this->assertTrue($clearedLogin);
+ $this->assertTrue($clearedState);
+ }
+}
diff --git a/tests/Core/Controller/ContactsMenuControllerTest.php b/tests/Core/Controller/ContactsMenuControllerTest.php
new file mode 100644
index 00000000000..aa20e6726e2
--- /dev/null
+++ b/tests/Core/Controller/ContactsMenuControllerTest.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Controller;
+
+use OC\Contacts\ContactsMenu\Manager;
+use OC\Core\Controller\ContactsMenuController;
+use OCP\Contacts\ContactsMenu\IEntry;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class ContactsMenuControllerTest extends TestCase {
+ /** @var IUserSession|MockObject */
+ private $userSession;
+
+ /** @var Manager|MockObject */
+ private $contactsManager;
+
+ private ContactsMenuController $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $request = $this->createMock(IRequest::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->contactsManager = $this->createMock(Manager::class);
+
+ $this->controller = new ContactsMenuController($request, $this->userSession, $this->contactsManager);
+ }
+
+ public function testIndex(): void {
+ $user = $this->createMock(IUser::class);
+ $entries = [
+ $this->createMock(IEntry::class),
+ $this->createMock(IEntry::class),
+ ];
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->contactsManager->expects($this->once())
+ ->method('getEntries')
+ ->with($this->equalTo($user), $this->equalTo(null))
+ ->willReturn($entries);
+
+ $response = $this->controller->index();
+
+ $this->assertEquals($entries, $response);
+ }
+
+ public function testFindOne(): void {
+ $user = $this->createMock(IUser::class);
+ $entry = $this->createMock(IEntry::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->contactsManager->expects($this->once())
+ ->method('findOne')
+ ->with($this->equalTo($user), $this->equalTo(42), $this->equalTo('test-search-phrase'))
+ ->willReturn($entry);
+
+ $response = $this->controller->findOne(42, 'test-search-phrase');
+
+ $this->assertEquals($entry, $response);
+ }
+
+ public function testFindOne404(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->contactsManager->expects($this->once())
+ ->method('findOne')
+ ->with($this->equalTo($user), $this->equalTo(42), $this->equalTo('test-search-phrase'))
+ ->willReturn(null);
+
+ $response = $this->controller->findOne(42, 'test-search-phrase');
+
+ $this->assertEquals([], $response->getData());
+ $this->assertEquals(404, $response->getStatus());
+ }
+}
diff --git a/tests/Core/Controller/CssControllerTest.php b/tests/Core/Controller/CssControllerTest.php
new file mode 100644
index 00000000000..b4764d6ea3a
--- /dev/null
+++ b/tests/Core/Controller/CssControllerTest.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Core\Controller\CssController;
+use OC\Files\AppData\AppData;
+use OC\Files\AppData\Factory;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\FileDisplayResponse;
+use OCP\AppFramework\Http\NotFoundResponse;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\IRequest;
+use Test\TestCase;
+
+class CssControllerTest extends TestCase {
+ /** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */
+ private $appData;
+
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+
+ /** @var CssController */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ /** @var Factory|\PHPUnit\Framework\MockObject\MockObject $factory */
+ $factory = $this->createMock(Factory::class);
+ $this->appData = $this->createMock(AppData::class);
+ $factory->expects($this->once())
+ ->method('get')
+ ->with('css')
+ ->willReturn($this->appData);
+
+ /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject $timeFactory */
+ $timeFactory = $this->createMock(ITimeFactory::class);
+ $timeFactory->method('getTime')
+ ->willReturn(1337);
+
+ $this->request = $this->createMock(IRequest::class);
+
+ $this->controller = new CssController(
+ 'core',
+ $this->request,
+ $factory,
+ $timeFactory
+ );
+ }
+
+ public function testNoCssFolderForApp(): void {
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willThrowException(new NotFoundException());
+
+ $result = $this->controller->getCss('file.css', 'myapp');
+
+ $this->assertInstanceOf(NotFoundResponse::class, $result);
+ }
+
+
+ public function testNoCssFile(): void {
+ $folder = $this->createMock(ISimpleFolder::class);
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willReturn($folder);
+
+ $folder->method('getFile')
+ ->willThrowException(new NotFoundException());
+
+ $result = $this->controller->getCss('file.css', 'myapp');
+
+ $this->assertInstanceOf(NotFoundResponse::class, $result);
+ }
+
+ public function testGetFile(): void {
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getName')->willReturn('my name');
+ $file->method('getMTime')->willReturn(42);
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willReturn($folder);
+
+ $folder->method('getFile')
+ ->with('file.css')
+ ->willReturn($file);
+
+ $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'text/css']);
+ $expected->addHeader('Cache-Control', 'max-age=31536000, immutable');
+ $expires = new \DateTime();
+ $expires->setTimestamp(1337);
+ $expires->add(new \DateInterval('PT31536000S'));
+ $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123));
+
+ $result = $this->controller->getCss('file.css', 'myapp');
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testGetGzipFile(): void {
+ $folder = $this->createMock(ISimpleFolder::class);
+ $gzipFile = $this->createMock(ISimpleFile::class);
+ $gzipFile->method('getName')->willReturn('my name');
+ $gzipFile->method('getMTime')->willReturn(42);
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willReturn($folder);
+
+ $folder->method('getFile')
+ ->with('file.css.gzip')
+ ->willReturn($gzipFile);
+
+ $this->request->method('getHeader')
+ ->with('Accept-Encoding')
+ ->willReturn('gzip, deflate');
+
+ $expected = new FileDisplayResponse($gzipFile, Http::STATUS_OK, ['Content-Type' => 'text/css']);
+ $expected->addHeader('Content-Encoding', 'gzip');
+ $expected->addHeader('Cache-Control', 'max-age=31536000, immutable');
+ $expires = new \DateTime();
+ $expires->setTimestamp(1337);
+ $expires->add(new \DateInterval('PT31536000S'));
+ $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123));
+
+ $result = $this->controller->getCss('file.css', 'myapp');
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testGetGzipFileNotFound(): void {
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getName')->willReturn('my name');
+ $file->method('getMTime')->willReturn(42);
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willReturn($folder);
+
+ $folder->method('getFile')
+ ->willReturnCallback(
+ function ($fileName) use ($file) {
+ if ($fileName === 'file.css') {
+ return $file;
+ }
+ throw new NotFoundException();
+ }
+ );
+
+ $this->request->method('getHeader')
+ ->with('Accept-Encoding')
+ ->willReturn('gzip, deflate');
+
+ $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'text/css']);
+ $expected->addHeader('Cache-Control', 'max-age=31536000, immutable');
+ $expires = new \DateTime();
+ $expires->setTimestamp(1337);
+ $expires->add(new \DateInterval('PT31536000S'));
+ $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123));
+
+ $result = $this->controller->getCss('file.css', 'myapp');
+ $this->assertEquals($expected, $result);
+ }
+}
diff --git a/tests/Core/Controller/GuestAvatarControllerTest.php b/tests/Core/Controller/GuestAvatarControllerTest.php
new file mode 100644
index 00000000000..66a83098130
--- /dev/null
+++ b/tests/Core/Controller/GuestAvatarControllerTest.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace Core\Controller;
+
+use OC\Core\Controller\GuestAvatarController;
+use OCP\AppFramework\Http\FileDisplayResponse;
+use OCP\Files\File;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\IAvatar;
+use OCP\IAvatarManager;
+use OCP\IRequest;
+use Psr\Log\LoggerInterface;
+
+/**
+ * This class provides tests for the guest avatar controller.
+ */
+class GuestAvatarControllerTest extends \Test\TestCase {
+ /**
+ * @var GuestAvatarController
+ */
+ private $guestAvatarController;
+
+ /**
+ * @var IRequest|\PHPUnit\Framework\MockObject\MockObject
+ */
+ private $request;
+
+ /**
+ * @var IAvatarManager|\PHPUnit\Framework\MockObject\MockObject
+ */
+ private $avatarManager;
+
+ /**
+ * @var IAvatar|\PHPUnit\Framework\MockObject\MockObject
+ */
+ private $avatar;
+
+ /**
+ * @var File|\PHPUnit\Framework\MockObject\MockObject
+ */
+ private $file;
+
+ /**
+ * @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject
+ */
+ private $logger;
+
+ /**
+ * Sets up the test environment.
+ */
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
+ $this->request = $this->getMockBuilder(IRequest::class)->getMock();
+ $this->avatar = $this->getMockBuilder(IAvatar::class)->getMock();
+ $this->avatarManager = $this->getMockBuilder(IAvatarManager::class)->getMock();
+ $this->file = $this->getMockBuilder(ISimpleFile::class)->getMock();
+ $this->file->method('getName')->willReturn('my name');
+ $this->file->method('getMTime')->willReturn(42);
+ $this->guestAvatarController = new GuestAvatarController(
+ 'core',
+ $this->request,
+ $this->avatarManager,
+ $this->logger
+ );
+ }
+
+ /**
+ * Tests getAvatar returns the guest avatar.
+ */
+ public function testGetAvatar(): void {
+ $this->avatarManager->expects($this->once())
+ ->method('getGuestAvatar')
+ ->with('Peter')
+ ->willReturn($this->avatar);
+
+ $this->avatar->expects($this->once())
+ ->method('getFile')
+ ->with(512)
+ ->willReturn($this->file);
+
+ $this->file->method('getMimeType')
+ ->willReturn('image/svg+xml');
+
+ $response = $this->guestAvatarController->getAvatar('Peter', 128);
+
+ $this->assertGreaterThanOrEqual(201, $response->getStatus());
+ $this->assertInstanceOf(FileDisplayResponse::class, $response);
+ }
+}
diff --git a/tests/Core/Controller/JsControllerTest.php b/tests/Core/Controller/JsControllerTest.php
new file mode 100644
index 00000000000..30bc02e8625
--- /dev/null
+++ b/tests/Core/Controller/JsControllerTest.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Core\Controller\JsController;
+use OC\Files\AppData\AppData;
+use OC\Files\AppData\Factory;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\FileDisplayResponse;
+use OCP\AppFramework\Http\NotFoundResponse;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Files\IAppData;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\IRequest;
+use Test\TestCase;
+
+class JsControllerTest extends TestCase {
+ /** @var IAppData|\PHPUnit\Framework\MockObject\MockObject */
+ private $appData;
+
+ /** @var JsController */
+ private $controller;
+
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ /** @var Factory|\PHPUnit\Framework\MockObject\MockObject $factory */
+ $factory = $this->createMock(Factory::class);
+ $this->appData = $this->createMock(AppData::class);
+ $factory->expects($this->once())
+ ->method('get')
+ ->with('js')
+ ->willReturn($this->appData);
+
+ /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject $timeFactory */
+ $timeFactory = $this->createMock(ITimeFactory::class);
+ $timeFactory->method('getTime')
+ ->willReturn(1337);
+
+ $this->request = $this->createMock(IRequest::class);
+
+ $this->controller = new JsController(
+ 'core',
+ $this->request,
+ $factory,
+ $timeFactory
+ );
+ }
+
+ public function testNoCssFolderForApp(): void {
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willThrowException(new NotFoundException());
+
+ $result = $this->controller->getJs('file.css', 'myapp');
+
+ $this->assertInstanceOf(NotFoundResponse::class, $result);
+ }
+
+
+ public function testNoCssFile(): void {
+ $folder = $this->createMock(ISimpleFolder::class);
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willReturn($folder);
+
+ $folder->method('getFile')
+ ->willThrowException(new NotFoundException());
+
+ $result = $this->controller->getJs('file.css', 'myapp');
+
+ $this->assertInstanceOf(NotFoundResponse::class, $result);
+ }
+
+ public function testGetFile(): void {
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getName')->willReturn('my name');
+ $file->method('getMTime')->willReturn(42);
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willReturn($folder);
+
+ $folder->method('getFile')
+ ->with('file.js')
+ ->willReturn($file);
+
+ $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'application/javascript']);
+ $expected->addHeader('Cache-Control', 'max-age=31536000, immutable');
+ $expires = new \DateTime();
+ $expires->setTimestamp(1337);
+ $expires->add(new \DateInterval('PT31536000S'));
+ $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123));
+
+ $result = $this->controller->getJs('file.js', 'myapp');
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testGetGzipFile(): void {
+ $folder = $this->createMock(ISimpleFolder::class);
+ $gzipFile = $this->createMock(ISimpleFile::class);
+ $gzipFile->method('getName')->willReturn('my name');
+ $gzipFile->method('getMTime')->willReturn(42);
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willReturn($folder);
+
+ $folder->method('getFile')
+ ->with('file.js.gzip')
+ ->willReturn($gzipFile);
+
+ $this->request->method('getHeader')
+ ->with('Accept-Encoding')
+ ->willReturn('gzip, deflate');
+
+ $expected = new FileDisplayResponse($gzipFile, Http::STATUS_OK, ['Content-Type' => 'application/javascript']);
+ $expected->addHeader('Content-Encoding', 'gzip');
+ $expected->addHeader('Cache-Control', 'max-age=31536000, immutable');
+ $expires = new \DateTime();
+ $expires->setTimestamp(1337);
+ $expires->add(new \DateInterval('PT31536000S'));
+ $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123));
+
+ $result = $this->controller->getJs('file.js', 'myapp');
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testGetGzipFileNotFound(): void {
+ $folder = $this->createMock(ISimpleFolder::class);
+ $file = $this->createMock(ISimpleFile::class);
+ $file->method('getName')->willReturn('my name');
+ $file->method('getMTime')->willReturn(42);
+ $this->appData->method('getFolder')
+ ->with('myapp')
+ ->willReturn($folder);
+
+ $folder->method('getFile')
+ ->willReturnCallback(
+ function ($fileName) use ($file) {
+ if ($fileName === 'file.js') {
+ return $file;
+ }
+ throw new NotFoundException();
+ }
+ );
+
+ $this->request->method('getHeader')
+ ->with('Accept-Encoding')
+ ->willReturn('gzip, deflate');
+
+ $expected = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => 'application/javascript']);
+ $expected->addHeader('Cache-Control', 'max-age=31536000, immutable');
+ $expires = new \DateTime();
+ $expires->setTimestamp(1337);
+ $expires->add(new \DateInterval('PT31536000S'));
+ $expected->addHeader('Expires', $expires->format(\DateTime::RFC1123));
+
+ $result = $this->controller->getJs('file.js', 'myapp');
+ $this->assertEquals($expected, $result);
+ }
+}
diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php
new file mode 100644
index 00000000000..18baaf5b08c
--- /dev/null
+++ b/tests/Core/Controller/LoginControllerTest.php
@@ -0,0 +1,696 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Authentication\Login\Chain as LoginChain;
+use OC\Authentication\Login\LoginData;
+use OC\Authentication\Login\LoginResult;
+use OC\Authentication\TwoFactorAuth\Manager;
+use OC\Core\Controller\LoginController;
+use OC\User\Session;
+use OCP\App\IAppManager;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\Defaults;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Notification\IManager;
+use OCP\Security\Bruteforce\IThrottler;
+use OCP\Security\ITrustedDomainHelper;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class LoginControllerTest extends TestCase {
+ /** @var LoginController */
+ private $loginController;
+
+ /** @var IRequest|MockObject */
+ private $request;
+
+ /** @var IUserManager|MockObject */
+ private $userManager;
+
+ /** @var IConfig|MockObject */
+ private $config;
+
+ /** @var ISession|MockObject */
+ private $session;
+
+ /** @var Session|MockObject */
+ private $userSession;
+
+ /** @var IURLGenerator|MockObject */
+ private $urlGenerator;
+
+ /** @var Manager|MockObject */
+ private $twoFactorManager;
+
+ /** @var Defaults|MockObject */
+ private $defaults;
+
+ /** @var IThrottler|MockObject */
+ private $throttler;
+
+ /** @var IInitialState|MockObject */
+ private $initialState;
+
+ /** @var \OC\Authentication\WebAuthn\Manager|MockObject */
+ private $webAuthnManager;
+
+ /** @var IManager|MockObject */
+ private $notificationManager;
+
+ /** @var IL10N|MockObject */
+ private $l;
+
+ /** @var IAppManager|MockObject */
+ private $appManager;
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->request = $this->createMock(IRequest::class);
+ $this->userManager = $this->createMock(\OC\User\Manager::class);
+ $this->config = $this->createMock(IConfig::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->userSession = $this->createMock(Session::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->twoFactorManager = $this->createMock(Manager::class);
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->throttler = $this->createMock(IThrottler::class);
+ $this->initialState = $this->createMock(IInitialState::class);
+ $this->webAuthnManager = $this->createMock(\OC\Authentication\WebAuthn\Manager::class);
+ $this->notificationManager = $this->createMock(IManager::class);
+ $this->l = $this->createMock(IL10N::class);
+ $this->appManager = $this->createMock(IAppManager::class);
+
+ $this->l->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($text, $parameters = []) {
+ return vsprintf($text, $parameters);
+ });
+
+
+ $this->request->method('getRemoteAddress')
+ ->willReturn('1.2.3.4');
+ $this->request->method('getHeader')
+ ->with('Origin')
+ ->willReturn('domain.example.com');
+ $this->throttler->method('getDelay')
+ ->with(
+ $this->equalTo('1.2.3.4'),
+ $this->equalTo('')
+ )->willReturn(1000);
+
+ $this->loginController = new LoginController(
+ 'core',
+ $this->request,
+ $this->userManager,
+ $this->config,
+ $this->session,
+ $this->userSession,
+ $this->urlGenerator,
+ $this->defaults,
+ $this->throttler,
+ $this->initialState,
+ $this->webAuthnManager,
+ $this->notificationManager,
+ $this->l,
+ $this->appManager,
+ );
+ }
+
+ public function testLogoutWithoutToken(): void {
+ $this->request
+ ->expects($this->once())
+ ->method('getCookie')
+ ->with('nc_token')
+ ->willReturn(null);
+ $this->request
+ ->method('getServerProtocol')
+ ->willReturn('https');
+ $this->request
+ ->expects($this->once())
+ ->method('isUserAgent')
+ ->willReturn(false);
+ $this->config
+ ->expects($this->never())
+ ->method('deleteUserValue');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('core.login.showLoginForm')
+ ->willReturn('/login');
+
+ $expected = new RedirectResponse('/login');
+ $expected->addHeader('Clear-Site-Data', '"cache", "storage"');
+ $this->assertEquals($expected, $this->loginController->logout());
+ }
+
+ public function testLogoutNoClearSiteData(): void {
+ $this->request
+ ->expects($this->once())
+ ->method('getCookie')
+ ->with('nc_token')
+ ->willReturn(null);
+ $this->request
+ ->method('getServerProtocol')
+ ->willReturn('https');
+ $this->request
+ ->expects($this->once())
+ ->method('isUserAgent')
+ ->willReturn(true);
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('core.login.showLoginForm')
+ ->willReturn('/login');
+
+ $expected = new RedirectResponse('/login');
+ $this->assertEquals($expected, $this->loginController->logout());
+ }
+
+ public function testLogoutWithToken(): void {
+ $this->request
+ ->expects($this->once())
+ ->method('getCookie')
+ ->with('nc_token')
+ ->willReturn('MyLoginToken');
+ $this->request
+ ->method('getServerProtocol')
+ ->willReturn('https');
+ $this->request
+ ->expects($this->once())
+ ->method('isUserAgent')
+ ->willReturn(false);
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('getUID')
+ ->willReturn('JohnDoe');
+ $this->userSession
+ ->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->config
+ ->expects($this->once())
+ ->method('deleteUserValue')
+ ->with('JohnDoe', 'login_token', 'MyLoginToken');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('core.login.showLoginForm')
+ ->willReturn('/login');
+
+ $expected = new RedirectResponse('/login');
+ $expected->addHeader('Clear-Site-Data', '"cache", "storage"');
+ $this->assertEquals($expected, $this->loginController->logout());
+ }
+
+ public function testShowLoginFormForLoggedInUsers(): void {
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToDefaultPageUrl')
+ ->willReturn('/default/foo');
+
+ $expectedResponse = new RedirectResponse('/default/foo');
+ $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', ''));
+ }
+
+ public function testShowLoginFormWithErrorsInSession(): void {
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->session
+ ->expects($this->once())
+ ->method('get')
+ ->with('loginMessages')
+ ->willReturn(
+ [
+ [
+ 'ErrorArray1',
+ 'ErrorArray2',
+ ],
+ [
+ 'MessageArray1',
+ 'MessageArray2',
+ ],
+ ]
+ );
+
+ $calls = [
+ [
+ 'loginMessages',
+ [
+ 'MessageArray1',
+ 'MessageArray2',
+ 'This community release of Nextcloud is unsupported and push notifications are limited.',
+ ],
+ ],
+ [
+ 'loginErrors',
+ [
+ 'ErrorArray1',
+ 'ErrorArray2',
+ ],
+ ],
+ [
+ 'loginUsername',
+ '',
+ ]
+ ];
+ $this->initialState->expects($this->exactly(13))
+ ->method('provideInitialState')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ if (!empty($expected)) {
+ $this->assertEquals($expected, func_get_args());
+ }
+ });
+
+ $expectedResponse = new TemplateResponse(
+ 'core',
+ 'login',
+ [
+ 'alt_login' => [],
+ 'pageTitle' => 'Login'
+ ],
+ 'guest'
+ );
+ $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', ''));
+ }
+
+ public function testShowLoginFormForFlowAuth(): void {
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $calls = [
+ [], [], [],
+ [
+ 'loginAutocomplete',
+ false
+ ],
+ [
+ 'loginRedirectUrl',
+ 'login/flow'
+ ],
+ ];
+ $this->initialState->expects($this->exactly(14))
+ ->method('provideInitialState')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ if (!empty($expected)) {
+ $this->assertEquals($expected, func_get_args());
+ }
+ });
+
+ $expectedResponse = new TemplateResponse(
+ 'core',
+ 'login',
+ [
+ 'alt_login' => [],
+ 'pageTitle' => 'Login'
+ ],
+ 'guest'
+ );
+ $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('', 'login/flow'));
+ }
+
+ /**
+ * @return array
+ */
+ public static function passwordResetDataProvider(): array {
+ return [
+ [
+ true,
+ true,
+ ],
+ [
+ false,
+ false,
+ ],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('passwordResetDataProvider')]
+ public function testShowLoginFormWithPasswordResetOption($canChangePassword,
+ $expectedResult): void {
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->config
+ ->expects(self::once())
+ ->method('getSystemValue')
+ ->willReturnMap([
+ ['login_form_autocomplete', true, true],
+ ]);
+ $this->config
+ ->expects(self::once())
+ ->method('getSystemValueString')
+ ->willReturnMap([
+ ['lost_password_link', '', ''],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user
+ ->expects($this->once())
+ ->method('canChangePassword')
+ ->willReturn($canChangePassword);
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('LdapUser')
+ ->willReturn($user);
+ $calls = [
+ [], [],
+ [
+ 'loginUsername',
+ 'LdapUser'
+ ],
+ [], [], [],
+ [
+ 'loginCanResetPassword',
+ $expectedResult
+ ],
+ ];
+ $this->initialState->expects($this->exactly(13))
+ ->method('provideInitialState')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ if (!empty($expected)) {
+ $this->assertEquals($expected, func_get_args());
+ }
+ });
+
+ $expectedResponse = new TemplateResponse(
+ 'core',
+ 'login',
+ [
+ 'alt_login' => [],
+ 'pageTitle' => 'Login'
+ ],
+ 'guest'
+ );
+ $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('LdapUser', ''));
+ }
+
+ public function testShowLoginFormForUserNamed0(): void {
+ $this->userSession
+ ->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $this->config
+ ->expects(self::once())
+ ->method('getSystemValue')
+ ->willReturnMap([
+ ['login_form_autocomplete', true, true],
+ ]);
+ $this->config
+ ->expects(self::once())
+ ->method('getSystemValueString')
+ ->willReturnMap([
+ ['lost_password_link', '', ''],
+ ]);
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->once())
+ ->method('canChangePassword')
+ ->willReturn(false);
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('0')
+ ->willReturn($user);
+ $calls = [
+ [], [], [],
+ [
+ 'loginAutocomplete',
+ true
+ ],
+ [],
+ [
+ 'loginResetPasswordLink',
+ false
+ ],
+ [
+ 'loginCanResetPassword',
+ false
+ ],
+ ];
+ $this->initialState->expects($this->exactly(13))
+ ->method('provideInitialState')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ if (!empty($expected)) {
+ $this->assertEquals($expected, func_get_args());
+ }
+ });
+
+ $expectedResponse = new TemplateResponse(
+ 'core',
+ 'login',
+ [
+ 'alt_login' => [],
+ 'pageTitle' => 'Login'
+ ],
+ 'guest'
+ );
+ $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('0', ''));
+ }
+
+ public function testLoginWithInvalidCredentials(): void {
+ $user = 'MyUserName';
+ $password = 'secret';
+ $loginPageUrl = '/login?redirect_url=/apps/files';
+ $loginChain = $this->createMock(LoginChain::class);
+ $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
+ $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+ $loginData = new LoginData(
+ $this->request,
+ $user,
+ $password,
+ '/apps/files'
+ );
+ $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
+ $loginChain->expects($this->once())
+ ->method('process')
+ ->with($this->equalTo($loginData))
+ ->willReturn($loginResult);
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.login.showLoginForm', [
+ 'user' => $user,
+ 'redirect_url' => '/apps/files',
+ 'direct' => 1,
+ ])
+ ->willReturn($loginPageUrl);
+ $expected = new RedirectResponse($loginPageUrl);
+ $expected->throttle(['user' => 'MyUserName']);
+
+ $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, '/apps/files');
+
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testLoginWithValidCredentials(): void {
+ $user = 'MyUserName';
+ $password = 'secret';
+ $loginChain = $this->createMock(LoginChain::class);
+ $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
+ $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+ $loginData = new LoginData(
+ $this->request,
+ $user,
+ $password
+ );
+ $loginResult = LoginResult::success($loginData);
+ $loginChain->expects($this->once())
+ ->method('process')
+ ->with($this->equalTo($loginData))
+ ->willReturn($loginResult);
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToDefaultPageUrl')
+ ->willReturn('/default/foo');
+
+ $expected = new RedirectResponse('/default/foo');
+ $this->assertEquals($expected, $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password));
+ }
+
+ public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn(): void {
+ /** @var IUser|MockObject $user */
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('jane');
+ $password = 'secret';
+ $originalUrl = 'another%20url';
+ $loginChain = $this->createMock(LoginChain::class);
+ $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
+ $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+ $this->userSession
+ ->method('isLoggedIn')
+ ->with()
+ ->willReturn(false);
+ $this->config->expects($this->never())
+ ->method('deleteUserValue');
+ $this->userSession->expects($this->never())
+ ->method('createRememberMeToken');
+
+ $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $originalUrl);
+
+ $expected = new RedirectResponse('');
+ $expected->throttle(['user' => 'Jane']);
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testLoginWithoutPassedCsrfCheckAndLoggedIn(): void {
+ /** @var IUser|MockObject $user */
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('getUID')
+ ->willReturn('jane');
+ $password = 'secret';
+ $originalUrl = 'another url';
+ $redirectUrl = 'http://localhost/another url';
+ $loginChain = $this->createMock(LoginChain::class);
+ $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
+ $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(false);
+ $this->userSession
+ ->method('isLoggedIn')
+ ->with()
+ ->willReturn(true);
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with(urldecode($originalUrl))
+ ->willReturn($redirectUrl);
+ $this->config->expects($this->never())
+ ->method('deleteUserValue');
+ $this->userSession->expects($this->never())
+ ->method('createRememberMeToken');
+ $this->config
+ ->method('getSystemValue')
+ ->with('remember_login_cookie_lifetime')
+ ->willReturn(1234);
+
+ $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, 'Jane', $password, $originalUrl);
+
+ $expected = new RedirectResponse($redirectUrl);
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testLoginWithValidCredentialsAndRedirectUrl(): void {
+ $user = 'MyUserName';
+ $password = 'secret';
+ $redirectUrl = 'https://next.cloud/apps/mail';
+ $loginChain = $this->createMock(LoginChain::class);
+ $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
+ $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+ $loginData = new LoginData(
+ $this->request,
+ $user,
+ $password,
+ '/apps/mail'
+ );
+ $loginResult = LoginResult::success($loginData);
+ $loginChain->expects($this->once())
+ ->method('process')
+ ->with($this->equalTo($loginData))
+ ->willReturn($loginResult);
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('/apps/mail')
+ ->willReturn($redirectUrl);
+ $expected = new RedirectResponse($redirectUrl);
+
+ $response = $this->loginController->tryLogin($loginChain, $trustedDomainHelper, $user, $password, '/apps/mail');
+
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testToNotLeakLoginName(): void {
+ $loginChain = $this->createMock(LoginChain::class);
+ $trustedDomainHelper = $this->createMock(ITrustedDomainHelper::class);
+ $trustedDomainHelper->method('isTrustedUrl')->willReturn(true);
+ $this->request
+ ->expects($this->once())
+ ->method('passesCSRFCheck')
+ ->willReturn(true);
+ $loginPageUrl = '/login?redirect_url=/apps/files';
+ $loginData = new LoginData(
+ $this->request,
+ 'john@doe.com',
+ 'just wrong',
+ '/apps/files'
+ );
+ $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD);
+ $loginChain->expects($this->once())
+ ->method('process')
+ ->with($this->equalTo($loginData))
+ ->willReturnCallback(function (LoginData $data) use ($loginResult) {
+ $data->setUsername('john');
+ return $loginResult;
+ });
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.login.showLoginForm', [
+ 'user' => 'john@doe.com',
+ 'redirect_url' => '/apps/files',
+ 'direct' => 1,
+ ])
+ ->willReturn($loginPageUrl);
+ $expected = new RedirectResponse($loginPageUrl);
+ $expected->throttle(['user' => 'john']);
+
+ $response = $this->loginController->tryLogin(
+ $loginChain,
+ $trustedDomainHelper,
+ 'john@doe.com',
+ 'just wrong',
+ '/apps/files'
+ );
+
+ $this->assertEquals($expected, $response);
+ }
+}
diff --git a/tests/Core/Controller/LostControllerTest.php b/tests/Core/Controller/LostControllerTest.php
new file mode 100644
index 00000000000..bbb5f2c2e54
--- /dev/null
+++ b/tests/Core/Controller/LostControllerTest.php
@@ -0,0 +1,756 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Authentication\TwoFactorAuth\Manager;
+use OC\Core\Controller\LostController;
+use OC\Core\Events\BeforePasswordResetEvent;
+use OC\Core\Events\PasswordResetEvent;
+use OC\Mail\Message;
+use OC\Security\RateLimiting\Limiter;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\Defaults;
+use OCP\Encryption\IEncryptionModule;
+use OCP\Encryption\IManager;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Mail\IEMailTemplate;
+use OCP\Mail\IMailer;
+use OCP\Security\VerificationToken\InvalidTokenException;
+use OCP\Security\VerificationToken\IVerificationToken;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+/**
+ * Class LostControllerTest
+ *
+ * @package OC\Core\Controller
+ */
+class LostControllerTest extends TestCase {
+ private LostController $lostController;
+ /** @var IUser */
+ private $existingUser;
+ /** @var IURLGenerator | MockObject */
+ private $urlGenerator;
+ /** @var IL10N */
+ private $l10n;
+ /** @var IUserManager | MockObject */
+ private $userManager;
+ /** @var Defaults */
+ private $defaults;
+ /** @var IConfig | MockObject */
+ private $config;
+ /** @var IMailer | MockObject */
+ private $mailer;
+ /** @var IManager|MockObject */
+ private $encryptionManager;
+ /** @var IRequest|MockObject */
+ private $request;
+ /** @var LoggerInterface|MockObject */
+ private $logger;
+ /** @var Manager|MockObject */
+ private $twofactorManager;
+ /** @var IInitialState|MockObject */
+ private $initialState;
+ /** @var IVerificationToken|MockObject */
+ private $verificationToken;
+ /** @var IEventDispatcher|MockObject */
+ private $eventDispatcher;
+ /** @var Limiter|MockObject */
+ private $limiter;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->existingUser = $this->createMock(IUser::class);
+ $this->existingUser->expects($this->any())
+ ->method('getEMailAddress')
+ ->willReturn('test@example.com');
+ $this->existingUser->expects($this->any())
+ ->method('getUID')
+ ->willReturn('ExistingUser');
+ $this->existingUser->expects($this->any())
+ ->method('getDisplayName')
+ ->willReturn('Existing User');
+ $this->existingUser->expects($this->any())
+ ->method('isEnabled')
+ ->willReturn(true);
+
+ $this->config = $this->createMock(IConfig::class);
+ $this->config->expects($this->any())
+ ->method('getSystemValue')
+ ->willReturnMap([
+ ['secret', null, 'SECRET'],
+ ['secret', '', 'SECRET'],
+ ['lost_password_link', '', ''],
+ ]);
+ $this->l10n = $this->createMock(IL10N::class);
+ $this->l10n
+ ->expects($this->any())
+ ->method('t')
+ ->willReturnCallback(function ($text, $parameters = []) {
+ return vsprintf($text, $parameters);
+ });
+ $this->defaults = $this->createMock(Defaults::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->mailer = $this->createMock(IMailer::class);
+ $this->request = $this->createMock(IRequest::class);
+ $this->encryptionManager = $this->createMock(IManager::class);
+ $this->encryptionManager->expects($this->any())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->twofactorManager = $this->createMock(Manager::class);
+ $this->initialState = $this->createMock(IInitialState::class);
+ $this->verificationToken = $this->createMock(IVerificationToken::class);
+ $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
+ $this->limiter = $this->createMock(Limiter::class);
+ $this->lostController = new LostController(
+ 'Core',
+ $this->request,
+ $this->urlGenerator,
+ $this->userManager,
+ $this->defaults,
+ $this->l10n,
+ $this->config,
+ 'lostpassword-noreply@localhost',
+ $this->encryptionManager,
+ $this->mailer,
+ $this->logger,
+ $this->twofactorManager,
+ $this->initialState,
+ $this->verificationToken,
+ $this->eventDispatcher,
+ $this->limiter
+ );
+ }
+
+ public function testResetFormTokenError(): void {
+ $this->userManager->method('get')
+ ->with('ValidTokenUser')
+ ->willReturn($this->existingUser);
+ $this->verificationToken->expects($this->once())
+ ->method('check')
+ ->with('12345:MySecretToken', $this->existingUser, 'lostpassword', 'test@example.com')
+ ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_DECRYPTION_ERROR));
+
+ $response = $this->lostController->resetform('12345:MySecretToken', 'ValidTokenUser');
+ $expectedResponse = new TemplateResponse('core',
+ 'error',
+ [
+ 'errors' => [
+ ['error' => 'Could not reset password because the token is invalid'],
+ ]
+ ],
+ 'guest');
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+ public function testResetFormValidToken(): void {
+ $this->userManager->method('get')
+ ->with('ValidTokenUser')
+ ->willReturn($this->existingUser);
+ $this->verificationToken->expects($this->once())
+ ->method('check')
+ ->with('MySecretToken', $this->existingUser, 'lostpassword', 'test@example.com');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('core.lost.setPassword', ['userId' => 'ValidTokenUser', 'token' => 'MySecretToken'])
+ ->willReturn('https://example.tld/index.php/lostpassword/set/sometoken/someuser');
+
+ $calls = [
+ ['resetPasswordUser', 'ValidTokenUser'],
+ ['resetPasswordTarget', 'https://example.tld/index.php/lostpassword/set/sometoken/someuser'],
+ ];
+ $this->initialState
+ ->expects($this->exactly(2))
+ ->method('provideInitialState')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ $response = $this->lostController->resetform('MySecretToken', 'ValidTokenUser');
+ $expectedResponse = new TemplateResponse('core',
+ 'login',
+ [],
+ 'guest');
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+ public function testEmailUnsuccessful(): void {
+ $existingUser = 'ExistingUser';
+ $nonExistingUser = 'NonExistingUser';
+ $this->userManager
+ ->expects($this->any())
+ ->method('userExists')
+ ->willReturnMap([
+ [true, $existingUser],
+ [false, $nonExistingUser]
+ ]);
+
+ $this->logger->expects($this->exactly(0))
+ ->method('error');
+ $this->logger->expects($this->exactly(2))
+ ->method('warning');
+
+ $this->userManager
+ ->method('getByEmail')
+ ->willReturn([]);
+
+ // With a non existing user
+ $response = $this->lostController->email($nonExistingUser);
+ $expectedResponse = new JSONResponse([
+ 'status' => 'success',
+ ]);
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+
+ // With no mail address
+ $this->config
+ ->expects($this->any())
+ ->method('getUserValue')
+ ->with($existingUser, 'settings', 'email')
+ ->willReturn(null);
+ $response = $this->lostController->email($existingUser);
+ $expectedResponse = new JSONResponse([
+ 'status' => 'success',
+ ]);
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+ public function testEmailSuccessful(): void {
+ $this->userManager
+ ->expects($this->any())
+ ->method('get')
+ ->with('ExistingUser')
+ ->willReturn($this->existingUser);
+ $this->verificationToken->expects($this->once())
+ ->method('create')
+ ->willReturn('ThisIsMaybeANotSoSecretToken!');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('core.lost.resetform', ['userId' => 'ExistingUser', 'token' => 'ThisIsMaybeANotSoSecretToken!'])
+ ->willReturn('https://example.tld/index.php/lostpassword/');
+ $message = $this->getMockBuilder('\OC\Mail\Message')
+ ->disableOriginalConstructor()->getMock();
+ $message
+ ->expects($this->once())
+ ->method('setTo')
+ ->with(['test@example.com' => 'Existing User']);
+ $message
+ ->expects($this->once())
+ ->method('setFrom')
+ ->with(['lostpassword-noreply@localhost' => null]);
+
+ $emailTemplate = $this->createMock(IEMailTemplate::class);
+ $emailTemplate->expects($this->any())
+ ->method('renderHtml')
+ ->willReturn('HTML body');
+ $emailTemplate->expects($this->any())
+ ->method('renderText')
+ ->willReturn('text body');
+
+ $message
+ ->expects($this->once())
+ ->method('useTemplate')
+ ->with($emailTemplate);
+
+ $this->mailer
+ ->expects($this->once())
+ ->method('createEMailTemplate')
+ ->willReturn($emailTemplate);
+ $this->mailer
+ ->expects($this->once())
+ ->method('createMessage')
+ ->willReturn($message);
+ $this->mailer
+ ->expects($this->once())
+ ->method('send')
+ ->with($message);
+
+ $response = $this->lostController->email('ExistingUser');
+ $expectedResponse = new JSONResponse(['status' => 'success']);
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+ public function testEmailWithMailSuccessful(): void {
+ $this->userManager
+ ->expects($this->any())
+ ->method('get')
+ ->with('test@example.com')
+ ->willReturn(null);
+ $this->userManager
+ ->expects($this->any())
+ ->method('getByEmail')
+ ->with('test@example.com')
+ ->willReturn([$this->existingUser]);
+ $this->verificationToken->expects($this->once())
+ ->method('create')
+ ->willReturn('ThisIsMaybeANotSoSecretToken!');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('core.lost.resetform', ['userId' => 'ExistingUser', 'token' => 'ThisIsMaybeANotSoSecretToken!'])
+ ->willReturn('https://example.tld/index.php/lostpassword/');
+ $message = $this->getMockBuilder('\OC\Mail\Message')
+ ->disableOriginalConstructor()->getMock();
+ $message
+ ->expects($this->once())
+ ->method('setTo')
+ ->with(['test@example.com' => 'Existing User']);
+ $message
+ ->expects($this->once())
+ ->method('setFrom')
+ ->with(['lostpassword-noreply@localhost' => null]);
+
+ $emailTemplate = $this->createMock(IEMailTemplate::class);
+ $emailTemplate->expects($this->any())
+ ->method('renderHtml')
+ ->willReturn('HTML body');
+ $emailTemplate->expects($this->any())
+ ->method('renderText')
+ ->willReturn('text body');
+
+ $message
+ ->expects($this->once())
+ ->method('useTemplate')
+ ->with($emailTemplate);
+
+ $this->mailer
+ ->expects($this->once())
+ ->method('createEMailTemplate')
+ ->willReturn($emailTemplate);
+ $this->mailer
+ ->expects($this->once())
+ ->method('createMessage')
+ ->willReturn($message);
+ $this->mailer
+ ->expects($this->once())
+ ->method('send')
+ ->with($message);
+
+ $response = $this->lostController->email('test@example.com');
+ $expectedResponse = new JSONResponse(['status' => 'success']);
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+ public function testEmailCantSendException(): void {
+ $this->userManager
+ ->expects($this->any())
+ ->method('get')
+ ->with('ExistingUser')
+ ->willReturn($this->existingUser);
+ $this->verificationToken->expects($this->once())
+ ->method('create')
+ ->willReturn('ThisIsMaybeANotSoSecretToken!');
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToRouteAbsolute')
+ ->with('core.lost.resetform', ['userId' => 'ExistingUser', 'token' => 'ThisIsMaybeANotSoSecretToken!'])
+ ->willReturn('https://example.tld/index.php/lostpassword/');
+ $message = $this->createMock(Message::class);
+ $message
+ ->expects($this->once())
+ ->method('setTo')
+ ->with(['test@example.com' => 'Existing User']);
+ $message
+ ->expects($this->once())
+ ->method('setFrom')
+ ->with(['lostpassword-noreply@localhost' => null]);
+
+ $emailTemplate = $this->createMock(IEMailTemplate::class);
+ $emailTemplate->expects($this->any())
+ ->method('renderHtml')
+ ->willReturn('HTML body');
+ $emailTemplate->expects($this->any())
+ ->method('renderText')
+ ->willReturn('text body');
+
+ $message
+ ->expects($this->once())
+ ->method('useTemplate')
+ ->with($emailTemplate);
+
+ $this->mailer
+ ->expects($this->once())
+ ->method('createEMailTemplate')
+ ->willReturn($emailTemplate);
+ $this->mailer
+ ->expects($this->once())
+ ->method('createMessage')
+ ->willReturn($message);
+ $this->mailer
+ ->expects($this->once())
+ ->method('send')
+ ->with($message)
+ ->willThrowException(new \Exception());
+
+ $this->logger->expects($this->exactly(1))
+ ->method('error');
+
+ $response = $this->lostController->email('ExistingUser');
+ $expectedResponse = new JSONResponse(['status' => 'success']);
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+ public function testSetPasswordUnsuccessful(): void {
+ $this->config->method('getUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword', null)
+ ->willReturn('encryptedData');
+ $this->existingUser->method('getLastLogin')
+ ->willReturn(12344);
+ $this->existingUser->expects($this->once())
+ ->method('setPassword')
+ ->with('NewPassword')
+ ->willReturn(false);
+ $this->userManager->method('get')
+ ->with('ValidTokenUser')
+ ->willReturn($this->existingUser);
+ $beforePasswordResetEvent = new BeforePasswordResetEvent($this->existingUser, 'NewPassword');
+ $this->eventDispatcher
+ ->expects($this->once())
+ ->method('dispatchTyped')
+ ->with($beforePasswordResetEvent);
+ $this->config->expects($this->never())
+ ->method('deleteUserValue');
+
+ $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', true);
+ $expectedResponse = ['status' => 'error', 'msg' => ''];
+ $this->assertSame($expectedResponse, $response->getData());
+ }
+
+ public function testSetPasswordSuccessful(): void {
+ $this->config->method('getUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword', null)
+ ->willReturn('encryptedData');
+ $this->existingUser->method('getLastLogin')
+ ->willReturn(12344);
+ $this->existingUser->expects($this->once())
+ ->method('setPassword')
+ ->with('NewPassword')
+ ->willReturn(true);
+ $this->userManager->method('get')
+ ->with('ValidTokenUser')
+ ->willReturn($this->existingUser);
+
+ $calls = [
+ [new BeforePasswordResetEvent($this->existingUser, 'NewPassword')],
+ [new PasswordResetEvent($this->existingUser, 'NewPassword')],
+ ];
+ $this->eventDispatcher
+ ->expects($this->exactly(2))
+ ->method('dispatchTyped')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+
+ $this->config->expects($this->once())
+ ->method('deleteUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword');
+
+ $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', true);
+ $expectedResponse = ['user' => 'ValidTokenUser', 'status' => 'success'];
+ $this->assertSame($expectedResponse, $response->getData());
+ }
+
+ public function testSetPasswordExpiredToken(): void {
+ $this->config->method('getUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword', null)
+ ->willReturn('encryptedData');
+ $this->userManager->method('get')
+ ->with('ValidTokenUser')
+ ->willReturn($this->existingUser);
+ $this->verificationToken->expects($this->atLeastOnce())
+ ->method('check')
+ ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_EXPIRED));
+
+ $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', true);
+ $expectedResponse = [
+ 'status' => 'error',
+ 'msg' => 'Could not reset password because the token is expired',
+ ];
+ $this->assertSame($expectedResponse, $response->getData());
+ }
+
+ public function testSetPasswordInvalidDataInDb(): void {
+ $this->config->method('getUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword', null)
+ ->willReturn('invalidEncryptedData');
+ $this->userManager
+ ->method('get')
+ ->with('ValidTokenUser')
+ ->willReturn($this->existingUser);
+ $this->verificationToken->expects($this->atLeastOnce())
+ ->method('check')
+ ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_INVALID_FORMAT));
+
+ $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', true);
+ $expectedResponse = [
+ 'status' => 'error',
+ 'msg' => 'Could not reset password because the token is invalid',
+ ];
+ $this->assertSame($expectedResponse, $response->getData());
+ }
+
+ public function testIsSetPasswordWithoutTokenFailing(): void {
+ $this->config->method('getUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword', null)
+ ->willReturn('aValidtoken');
+ $this->userManager->method('get')
+ ->with('ValidTokenUser')
+ ->willReturn($this->existingUser);
+ $this->verificationToken->expects($this->atLeastOnce())
+ ->method('check')
+ ->willThrowException(new InvalidTokenException(InvalidTokenException::TOKEN_MISMATCH));
+
+ $response = $this->lostController->setPassword('', 'ValidTokenUser', 'NewPassword', true);
+ $expectedResponse = [
+ 'status' => 'error',
+ 'msg' => 'Could not reset password because the token is invalid'
+ ];
+ $this->assertSame($expectedResponse, $response->getData());
+ }
+
+ public function testSetPasswordForDisabledUser(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('isEnabled')
+ ->willReturn(false);
+ $user->expects($this->never())
+ ->method('setPassword');
+ $user->expects($this->any())
+ ->method('getEMailAddress')
+ ->willReturn('random@example.org');
+
+ $this->config->method('getUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword', null)
+ ->willReturn('encryptedData');
+ $this->userManager->method('get')
+ ->with('DisabledUser')
+ ->willReturn($user);
+
+ $this->verificationToken->expects($this->atLeastOnce())
+ ->method('check')
+ ->willThrowException(new InvalidTokenException(InvalidTokenException::USER_UNKNOWN));
+
+ $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'DisabledUser', 'NewPassword', true);
+ $expectedResponse = [
+ 'status' => 'error',
+ 'msg' => 'Could not reset password because the token is invalid'
+ ];
+ $this->assertSame($expectedResponse, $response->getData());
+ }
+
+ public function testSendEmailNoEmail(): void {
+ $user = $this->createMock(IUser::class);
+ $user->expects($this->any())
+ ->method('isEnabled')
+ ->willReturn(true);
+ $this->userManager->method('userExists')
+ ->with('ExistingUser')
+ ->willReturn(true);
+ $this->userManager->method('get')
+ ->with('ExistingUser')
+ ->willReturn($user);
+
+ $this->logger->expects($this->exactly(0))
+ ->method('error');
+ $this->logger->expects($this->once())
+ ->method('warning');
+
+ $response = $this->lostController->email('ExistingUser');
+ $expectedResponse = new JSONResponse(['status' => 'success']);
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+ public function testSetPasswordEncryptionDontProceedPerUserKey(): void {
+ /** @var IEncryptionModule|MockObject $encryptionModule */
+ $encryptionModule = $this->createMock(IEncryptionModule::class);
+ $encryptionModule->expects($this->once())->method('needDetailedAccessList')->willReturn(true);
+ $this->encryptionManager->expects($this->once())->method('getEncryptionModules')
+ ->willReturn([0 => ['callback' => function () use ($encryptionModule) {
+ return $encryptionModule;
+ }]]);
+ $response = $this->lostController->setPassword('myToken', 'user', 'newpass', false);
+ $expectedResponse = ['status' => 'error', 'msg' => '', 'encryption' => true];
+ $this->assertSame($expectedResponse, $response->getData());
+ }
+
+ public function testSetPasswordDontProceedMasterKey(): void {
+ $encryptionModule = $this->createMock(IEncryptionModule::class);
+ $encryptionModule->expects($this->once())->method('needDetailedAccessList')->willReturn(false);
+ $this->encryptionManager->expects($this->once())->method('getEncryptionModules')
+ ->willReturn([0 => ['callback' => function () use ($encryptionModule) {
+ return $encryptionModule;
+ }]]);
+ $this->config->method('getUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword', null)
+ ->willReturn('encryptedData');
+ $this->existingUser->method('getLastLogin')
+ ->willReturn(12344);
+ $this->existingUser->expects($this->once())
+ ->method('setPassword')
+ ->with('NewPassword')
+ ->willReturn(true);
+ $this->userManager->method('get')
+ ->with('ValidTokenUser')
+ ->willReturn($this->existingUser);
+ $this->config->expects($this->once())
+ ->method('deleteUserValue')
+ ->with('ValidTokenUser', 'core', 'lostpassword');
+
+ $response = $this->lostController->setPassword('TheOnlyAndOnlyOneTokenToResetThePassword', 'ValidTokenUser', 'NewPassword', false);
+ $expectedResponse = ['user' => 'ValidTokenUser', 'status' => 'success'];
+ $this->assertSame($expectedResponse, $response->getData());
+ }
+
+ public function testTwoUsersWithSameEmail(): void {
+ $user1 = $this->createMock(IUser::class);
+ $user1->expects($this->any())
+ ->method('getEMailAddress')
+ ->willReturn('test@example.com');
+ $user1->expects($this->any())
+ ->method('getUID')
+ ->willReturn('User1');
+ $user1->expects($this->any())
+ ->method('isEnabled')
+ ->willReturn(true);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->expects($this->any())
+ ->method('getEMailAddress')
+ ->willReturn('test@example.com');
+ $user2->expects($this->any())
+ ->method('getUID')
+ ->willReturn('User2');
+ $user2->expects($this->any())
+ ->method('isEnabled')
+ ->willReturn(true);
+
+ $this->userManager
+ ->method('get')
+ ->willReturn(null);
+
+ $this->userManager
+ ->method('getByEmail')
+ ->willReturn([$user1, $user2]);
+
+ $this->logger->expects($this->exactly(0))
+ ->method('error');
+ $this->logger->expects($this->once())
+ ->method('warning');
+
+ // request password reset for test@example.com
+ $response = $this->lostController->email('test@example.com');
+
+ $expectedResponse = new JSONResponse([
+ 'status' => 'success'
+ ]);
+ $expectedResponse->throttle();
+
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+
+ /**
+ * @return array
+ */
+ public static function dataTwoUsersWithSameEmailOneDisabled(): array {
+ return [
+ ['userEnabled1' => true, 'userEnabled2' => false],
+ ['userEnabled1' => false, 'userEnabled2' => true]
+ ];
+ }
+
+ /**
+ * @param bool $userEnabled1
+ * @param bool $userEnabled2
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTwoUsersWithSameEmailOneDisabled')]
+ public function testTwoUsersWithSameEmailOneDisabled(bool $userEnabled1, bool $userEnabled2): void {
+ $user1 = $this->createMock(IUser::class);
+ $user1->method('getEMailAddress')
+ ->willReturn('test@example.com');
+ $user1->method('getUID')
+ ->willReturn('User1');
+ $user1->method('isEnabled')
+ ->willReturn($userEnabled1);
+
+ $user2 = $this->createMock(IUser::class);
+ $user2->method('getEMailAddress')
+ ->willReturn('test@example.com');
+ $user2->method('getUID')
+ ->willReturn('User2');
+ $user2->method('isEnabled')
+ ->willReturn($userEnabled2);
+
+ $this->userManager
+ ->method('get')
+ ->willReturn(null);
+
+ $this->userManager
+ ->method('getByEmail')
+ ->willReturn([$user1, $user2]);
+
+ $result = self::invokePrivate($this->lostController, 'findUserByIdOrMail', ['test@example.com']);
+ $this->assertInstanceOf(IUser::class, $result);
+ }
+
+ public function testTrimEmailInput(): void {
+ $this->userManager
+ ->expects($this->once())
+ ->method('getByEmail')
+ ->with('test@example.com')
+ ->willReturn([$this->existingUser]);
+
+ $this->mailer
+ ->expects($this->once())
+ ->method('send');
+
+ $response = $this->lostController->email(' test@example.com ');
+ $expectedResponse = new JSONResponse(['status' => 'success']);
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+ }
+
+ public function testUsernameInput(): void {
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('ExistingUser')
+ ->willReturn($this->existingUser);
+
+ $this->mailer
+ ->expects($this->once())
+ ->method('send');
+
+ $response = $this->lostController->email(' ExistingUser ');
+ $expectedResponse = new JSONResponse(['status' => 'success']);
+ $expectedResponse->throttle();
+ $this->assertEquals($expectedResponse, $response);
+ }
+}
diff --git a/tests/Core/Controller/NavigationControllerTest.php b/tests/Core/Controller/NavigationControllerTest.php
new file mode 100644
index 00000000000..d00976f18ec
--- /dev/null
+++ b/tests/Core/Controller/NavigationControllerTest.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Core\Controller\NavigationController;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\INavigationManager;
+use OCP\IRequest;
+use OCP\IURLGenerator;
+use Test\TestCase;
+
+class NavigationControllerTest extends TestCase {
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+
+ /** @var INavigationManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $navigationManager;
+
+ /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
+ private $urlGenerator;
+
+ /** @var NavigationController */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->navigationManager = $this->createMock(INavigationManager::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+
+ $this->controller = new NavigationController(
+ 'core',
+ $this->request,
+ $this->navigationManager,
+ $this->urlGenerator
+ );
+ }
+
+ public static function dataGetNavigation(): array {
+ return [
+ [false],
+ [true],
+ ];
+ }
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGetNavigation')]
+ public function testGetAppNavigation(bool $absolute): void {
+ $this->navigationManager->expects($this->once())
+ ->method('getAll')
+ ->with('link')
+ ->willReturn(['files' => ['id' => 'files', 'href' => '/index.php/apps/files', 'icon' => 'icon' ] ]);
+ if ($absolute) {
+ $this->urlGenerator->expects($this->any())
+ ->method('getBaseURL')
+ ->willReturn('http://localhost/');
+ $this->urlGenerator->expects($this->exactly(2))
+ ->method('getAbsoluteURL')
+ ->willReturnMap([
+ ['/index.php/apps/files', 'http://localhost/index.php/apps/files'],
+ ['icon', 'http://localhost/icon'],
+ ]);
+ $actual = $this->controller->getAppsNavigation($absolute);
+ $this->assertInstanceOf(DataResponse::class, $actual);
+ $this->assertEquals('http://localhost/index.php/apps/files', $actual->getData()[0]['href']);
+ $this->assertEquals('http://localhost/icon', $actual->getData()[0]['icon']);
+ } else {
+ $actual = $this->controller->getAppsNavigation($absolute);
+ $this->assertInstanceOf(DataResponse::class, $actual);
+ $this->assertEquals('/index.php/apps/files', $actual->getData()[0]['href']);
+ $this->assertEquals('icon', $actual->getData()[0]['icon']);
+ }
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataGetNavigation')]
+ public function testGetSettingsNavigation(bool $absolute): void {
+ $this->navigationManager->expects($this->once())
+ ->method('getAll')
+ ->with('settings')
+ ->willReturn(['settings' => ['id' => 'settings', 'href' => '/index.php/settings/user', 'icon' => '/core/img/settings.svg'] ]);
+ if ($absolute) {
+ $this->urlGenerator->expects($this->any())
+ ->method('getBaseURL')
+ ->willReturn('http://localhost/');
+ $this->urlGenerator->expects($this->exactly(2))
+ ->method('getAbsoluteURL')
+ ->willReturnMap([
+ ['/index.php/settings/user', 'http://localhost/index.php/settings/user'],
+ ['/core/img/settings.svg', 'http://localhost/core/img/settings.svg']
+ ]);
+ $actual = $this->controller->getSettingsNavigation($absolute);
+ $this->assertInstanceOf(DataResponse::class, $actual);
+ $this->assertEquals('http://localhost/index.php/settings/user', $actual->getData()[0]['href']);
+ $this->assertEquals('http://localhost/core/img/settings.svg', $actual->getData()[0]['icon']);
+ } else {
+ $actual = $this->controller->getSettingsNavigation($absolute);
+ $this->assertInstanceOf(DataResponse::class, $actual);
+ $this->assertEquals('/index.php/settings/user', $actual->getData()[0]['href']);
+ $this->assertEquals('/core/img/settings.svg', $actual->getData()[0]['icon']);
+ }
+ }
+
+ public function testEtagIgnoresLogout(): void {
+ $navigation1 = [
+ ['id' => 'files', 'href' => '/index.php/apps/files', 'icon' => 'icon' ],
+ ['id' => 'logout', 'href' => '/index.php/logout?requesttoken=abcd', 'icon' => 'icon' ],
+ ];
+ $navigation2 = [
+ ['id' => 'files', 'href' => '/index.php/apps/files', 'icon' => 'icon' ],
+ ['id' => 'logout', 'href' => '/index.php/logout?requesttoken=1234', 'icon' => 'icon' ],
+ ];
+ $navigation3 = [
+ ['id' => 'files', 'href' => '/index.php/apps/files/test', 'icon' => 'icon' ],
+ ['id' => 'logout', 'href' => '/index.php/logout?requesttoken=1234', 'icon' => 'icon' ],
+ ];
+ $this->navigationManager->expects($this->exactly(3))
+ ->method('getAll')
+ ->with('link')
+ ->willReturnOnConsecutiveCalls(
+ $navigation1,
+ $navigation2,
+ $navigation3,
+ );
+
+ // Changes in the logout url should not change the ETag
+ $request1 = $this->controller->getAppsNavigation();
+ $request2 = $this->controller->getAppsNavigation();
+ $this->assertEquals($request1->getETag(), $request2->getETag());
+
+ // Changes in non-logout urls should result in a different ETag
+ $request3 = $this->controller->getAppsNavigation();
+ $this->assertNotEquals($request2->getETag(), $request3->getETag());
+ }
+}
diff --git a/tests/Core/Controller/OCSControllerTest.php b/tests/Core/Controller/OCSControllerTest.php
new file mode 100644
index 00000000000..bd7e26d5e8f
--- /dev/null
+++ b/tests/Core/Controller/OCSControllerTest.php
@@ -0,0 +1,218 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Core\Controller;
+
+use OC\CapabilitiesManager;
+use OC\Security\IdentityProof\Key;
+use OC\Security\IdentityProof\Manager;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Server;
+use OCP\ServerVersion;
+use Test\TestCase;
+
+class OCSControllerTest extends TestCase {
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+ /** @var CapabilitiesManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $capabilitiesManager;
+ /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
+ private $userSession;
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $userManager;
+ /** @var Manager|\PHPUnit\Framework\MockObject\MockObject */
+ private $keyManager;
+ /** @var ServerVersion|\PHPUnit\Framework\MockObject\MockObject */
+ private $serverVersion;
+ /** @var OCSController */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->capabilitiesManager = $this->createMock(CapabilitiesManager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->keyManager = $this->createMock(Manager::class);
+ $serverVersion = Server::get(ServerVersion::class);
+
+ $this->controller = new OCSController(
+ 'core',
+ $this->request,
+ $this->capabilitiesManager,
+ $this->userSession,
+ $this->userManager,
+ $this->keyManager,
+ $serverVersion
+ );
+ }
+
+ public function testGetConfig() {
+ $this->request->method('getServerHost')
+ ->willReturn('awesomehost.io');
+
+ $data = [
+ 'version' => '1.7',
+ 'website' => 'Nextcloud',
+ 'host' => 'awesomehost.io',
+ 'contact' => '',
+ 'ssl' => 'false',
+ ];
+
+ $expected = new DataResponse($data);
+ $this->assertEquals($expected, $this->controller->getConfig());
+
+ return new DataResponse($data);
+ }
+
+ public function testGetCapabilities(): void {
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+
+ $serverVersion = Server::get(ServerVersion::class);
+
+ $result = [];
+ $result['version'] = [
+ 'major' => $serverVersion->getMajorVersion(),
+ 'minor' => $serverVersion->getMinorVersion(),
+ 'micro' => $serverVersion->getPatchVersion(),
+ 'string' => $serverVersion->getVersionString(),
+ 'edition' => '',
+ 'extendedSupport' => false
+ ];
+
+ $capabilities = [
+ 'foo' => 'bar',
+ 'a' => [
+ 'b' => true,
+ 'c' => 11,
+ ]
+ ];
+ $this->capabilitiesManager->method('getCapabilities')
+ ->willReturn($capabilities);
+
+ $result['capabilities'] = $capabilities;
+
+ $expected = new DataResponse($result);
+ $expected->setETag(md5(json_encode($result)));
+ $this->assertEquals($expected, $this->controller->getCapabilities());
+ }
+
+ public function testGetCapabilitiesPublic(): void {
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+ $serverVersion = Server::get(ServerVersion::class);
+
+ $result = [];
+ $result['version'] = [
+ 'major' => $serverVersion->getMajorVersion(),
+ 'minor' => $serverVersion->getMinorVersion(),
+ 'micro' => $serverVersion->getPatchVersion(),
+ 'string' => $serverVersion->getVersionString(),
+ 'edition' => '',
+ 'extendedSupport' => false
+ ];
+
+ $capabilities = [
+ 'foo' => 'bar',
+ 'a' => [
+ 'b' => true,
+ 'c' => 11,
+ ]
+ ];
+ $this->capabilitiesManager->method('getCapabilities')
+ ->with(true)
+ ->willReturn($capabilities);
+
+ $result['capabilities'] = $capabilities;
+
+ $expected = new DataResponse($result);
+ $expected->setETag(md5(json_encode($result)));
+ $this->assertEquals($expected, $this->controller->getCapabilities());
+ }
+
+ public function testPersonCheckValid(): void {
+ $this->userManager->method('checkPassword')
+ ->with(
+ $this->equalTo('user'),
+ $this->equalTo('pass')
+ )->willReturn($this->createMock(IUser::class));
+
+ $expected = new DataResponse([
+ 'person' => [
+ 'personid' => 'user'
+ ]
+ ]);
+ $this->assertEquals($expected, $this->controller->personCheck('user', 'pass'));
+ }
+
+ public function testPersonInvalid(): void {
+ $this->userManager->method('checkPassword')
+ ->with(
+ $this->equalTo('user'),
+ $this->equalTo('wrongpass')
+ )->willReturn(false);
+
+ $expected = new DataResponse([], 102);
+ $expected->throttle();
+ $this->assertEquals($expected, $this->controller->personCheck('user', 'wrongpass'));
+ }
+
+ public function testPersonNoLogin(): void {
+ $this->userManager->method('checkPassword')
+ ->with(
+ $this->equalTo('user'),
+ $this->equalTo('wrongpass')
+ )->willReturn(false);
+
+ $expected = new DataResponse([], 101);
+ $this->assertEquals($expected, $this->controller->personCheck('', ''));
+ }
+
+ public function testGetIdentityProofWithNotExistingUser(): void {
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('NotExistingUser')
+ ->willReturn(null);
+
+ $expected = new DataResponse(['Account not found'], 404);
+ $this->assertEquals($expected, $this->controller->getIdentityProof('NotExistingUser'));
+ }
+
+ public function testGetIdentityProof(): void {
+ $user = $this->createMock(IUser::class);
+ $key = $this->createMock(Key::class);
+ $this->userManager
+ ->expects($this->once())
+ ->method('get')
+ ->with('ExistingUser')
+ ->willReturn($user);
+ $this->keyManager
+ ->expects($this->once())
+ ->method('getKey')
+ ->with($user)
+ ->willReturn($key);
+ $key
+ ->expects($this->once())
+ ->method('getPublic')
+ ->willReturn('Existing Users public key');
+
+ $expected = new DataResponse([
+ 'public' => 'Existing Users public key',
+ ]);
+ $this->assertEquals($expected, $this->controller->getIdentityProof('ExistingUser'));
+ }
+}
diff --git a/tests/Core/Controller/PreviewControllerTest.php b/tests/Core/Controller/PreviewControllerTest.php
new file mode 100644
index 00000000000..5a6cd1fba0a
--- /dev/null
+++ b/tests/Core/Controller/PreviewControllerTest.php
@@ -0,0 +1,367 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Core\Controller\PreviewController;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFile;
+use OCP\Files\Storage\ISharedStorage;
+use OCP\Files\Storage\IStorage;
+use OCP\IPreview;
+use OCP\IRequest;
+use OCP\Preview\IMimeIconProvider;
+use OCP\Share\IShare;
+use PHPUnit\Framework\MockObject\MockObject;
+
+class PreviewControllerTest extends \Test\TestCase {
+
+ private string $userId;
+ private PreviewController $controller;
+
+ private IRootFolder&MockObject $rootFolder;
+ private IPreview&MockObject $previewManager;
+ private IRequest&MockObject $request;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userId = 'user';
+ $this->rootFolder = $this->createMock(IRootFolder::class);
+ $this->previewManager = $this->createMock(IPreview::class);
+ $this->request = $this->createMock(IRequest::class);
+
+ $this->controller = new PreviewController(
+ 'core',
+ $this->request,
+ $this->previewManager,
+ $this->rootFolder,
+ $this->userId,
+ $this->createMock(IMimeIconProvider::class)
+ );
+ }
+
+ public function testInvalidFile(): void {
+ $res = $this->controller->getPreview('');
+ $expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testInvalidWidth(): void {
+ $res = $this->controller->getPreview('file', 0);
+ $expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testInvalidHeight(): void {
+ $res = $this->controller->getPreview('file', 10, 0);
+ $expected = new DataResponse([], Http::STATUS_BAD_REQUEST);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testFileNotFound(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willThrowException(new NotFoundException());
+
+ $res = $this->controller->getPreview('file');
+ $expected = new DataResponse([], Http::STATUS_NOT_FOUND);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testNotAFile(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $folder = $this->createMock(Folder::class);
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willReturn($folder);
+
+ $res = $this->controller->getPreview('file');
+ $expected = new DataResponse([], Http::STATUS_NOT_FOUND);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testNoPreviewAndNoIcon(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $file = $this->createMock(File::class);
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willReturn($file);
+
+ $this->previewManager->method('isAvailable')
+ ->with($this->equalTo($file))
+ ->willReturn(false);
+
+ $res = $this->controller->getPreview('file', 10, 10, true, false);
+ $expected = new DataResponse([], Http::STATUS_NOT_FOUND);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testNoPreview() {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $file = $this->createMock(File::class);
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willReturn($file);
+
+ $storage = $this->createMock(IStorage::class);
+ $file->method('getStorage')
+ ->willReturn($storage);
+
+ $this->previewManager->method('isAvailable')
+ ->with($this->equalTo($file))
+ ->willReturn(true);
+
+ $file->method('isReadable')
+ ->willReturn(true);
+
+ $this->previewManager->method('getPreview')
+ ->with($this->equalTo($file), 10, 10, false, $this->equalTo('myMode'))
+ ->willThrowException(new NotFoundException());
+
+ $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode');
+ $expected = new DataResponse([], Http::STATUS_NOT_FOUND);
+
+ $this->assertEquals($expected, $res);
+ }
+ public function testFileWithoutReadPermission() {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $file = $this->createMock(File::class);
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willReturn($file);
+
+ $this->previewManager->method('isAvailable')
+ ->with($this->equalTo($file))
+ ->willReturn(true);
+
+ $file->method('isReadable')
+ ->willReturn(false);
+
+ $res = $this->controller->getPreview('file', 10, 10, true, true);
+ $expected = new DataResponse([], Http::STATUS_FORBIDDEN);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testFileWithoutDownloadPermission() {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $file = $this->createMock(File::class);
+ $file->method('getId')->willReturn(123);
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willReturn($file);
+
+ $this->previewManager->method('isAvailable')
+ ->with($this->equalTo($file))
+ ->willReturn(true);
+
+ $share = $this->createMock(IShare::class);
+ $share->method('canSeeContent')
+ ->willReturn(false);
+
+ $storage = $this->createMock(ISharedStorage::class);
+ $storage->method('instanceOfStorage')
+ ->with(ISharedStorage::class)
+ ->willReturn(true);
+ $storage->method('getShare')
+ ->willReturn($share);
+
+ $file->method('getStorage')
+ ->willReturn($storage);
+ $file->method('isReadable')
+ ->willReturn(true);
+
+ $this->request->method('getHeader')->willReturn('');
+
+ $res = $this->controller->getPreview('file', 10, 10, true, true);
+ $expected = new DataResponse([], Http::STATUS_FORBIDDEN);
+
+ $this->assertEquals($expected, $res);
+ }
+
+ public function testFileWithoutDownloadPermissionButHeader() {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $file = $this->createMock(File::class);
+ $file->method('getId')->willReturn(123);
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willReturn($file);
+
+ $this->previewManager->method('isAvailable')
+ ->with($this->equalTo($file))
+ ->willReturn(true);
+
+ $share = $this->createMock(IShare::class);
+ $share->method('canSeeContent')
+ ->willReturn(false);
+
+ $storage = $this->createMock(ISharedStorage::class);
+ $storage->method('instanceOfStorage')
+ ->with(ISharedStorage::class)
+ ->willReturn(true);
+ $storage->method('getShare')
+ ->willReturn($share);
+
+ $file->method('getStorage')
+ ->willReturn($storage);
+ $file->method('isReadable')
+ ->willReturn(true);
+
+ $this->request
+ ->method('getHeader')
+ ->with('x-nc-preview')
+ ->willReturn('true');
+
+ $preview = $this->createMock(ISimpleFile::class);
+ $preview->method('getName')->willReturn('my name');
+ $preview->method('getMTime')->willReturn(42);
+ $this->previewManager->method('getPreview')
+ ->with($this->equalTo($file), 10, 10, false, $this->equalTo('myMode'))
+ ->willReturn($preview);
+ $preview->method('getMimeType')
+ ->willReturn('myMime');
+
+ $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode');
+
+ $this->assertEquals('myMime', $res->getHeaders()['Content-Type']);
+ $this->assertEquals(Http::STATUS_OK, $res->getStatus());
+ $this->assertEquals($preview, $this->invokePrivate($res, 'file'));
+ }
+
+ public function testValidPreview(): void {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $file = $this->createMock(File::class);
+ $file->method('getId')->willReturn(123);
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willReturn($file);
+
+ $this->previewManager->method('isAvailable')
+ ->with($this->equalTo($file))
+ ->willReturn(true);
+
+ $file->method('isReadable')
+ ->willReturn(true);
+
+ $storage = $this->createMock(IStorage::class);
+ $file->method('getStorage')
+ ->willReturn($storage);
+
+ $preview = $this->createMock(ISimpleFile::class);
+ $preview->method('getName')->willReturn('my name');
+ $preview->method('getMTime')->willReturn(42);
+ $this->previewManager->method('getPreview')
+ ->with($this->equalTo($file), 10, 10, false, $this->equalTo('myMode'))
+ ->willReturn($preview);
+ $preview->method('getMimeType')
+ ->willReturn('myMime');
+
+ $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode');
+
+ $this->assertEquals('myMime', $res->getHeaders()['Content-Type']);
+ $this->assertEquals(Http::STATUS_OK, $res->getStatus());
+ $this->assertEquals($preview, $this->invokePrivate($res, 'file'));
+ }
+
+ public function testValidPreviewOfShare() {
+ $userFolder = $this->createMock(Folder::class);
+ $this->rootFolder->method('getUserFolder')
+ ->with($this->equalTo($this->userId))
+ ->willReturn($userFolder);
+
+ $file = $this->createMock(File::class);
+ $file->method('getId')->willReturn(123);
+ $userFolder->method('get')
+ ->with($this->equalTo('file'))
+ ->willReturn($file);
+
+ $this->previewManager->method('isAvailable')
+ ->with($this->equalTo($file))
+ ->willReturn(true);
+
+ // No attributes set -> download permitted
+ $share = $this->createMock(IShare::class);
+ $share->method('canSeeContent')
+ ->willReturn(true);
+
+ $storage = $this->createMock(ISharedStorage::class);
+ $storage->method('instanceOfStorage')
+ ->with(ISharedStorage::class)
+ ->willReturn(true);
+ $storage->method('getShare')
+ ->willReturn($share);
+
+ $file->method('getStorage')
+ ->willReturn($storage);
+ $file->method('isReadable')
+ ->willReturn(true);
+
+ $this->request
+ ->method('getHeader')
+ ->willReturn('');
+
+ $preview = $this->createMock(ISimpleFile::class);
+ $preview->method('getName')->willReturn('my name');
+ $preview->method('getMTime')->willReturn(42);
+ $this->previewManager->method('getPreview')
+ ->with($this->equalTo($file), 10, 10, false, $this->equalTo('myMode'))
+ ->willReturn($preview);
+ $preview->method('getMimeType')
+ ->willReturn('myMime');
+
+ $res = $this->controller->getPreview('file', 10, 10, true, true, 'myMode');
+
+ $this->assertEquals('myMime', $res->getHeaders()['Content-Type']);
+ $this->assertEquals(Http::STATUS_OK, $res->getStatus());
+ $this->assertEquals($preview, $this->invokePrivate($res, 'file'));
+ }
+}
diff --git a/tests/Core/Controller/TwoFactorChallengeControllerTest.php b/tests/Core/Controller/TwoFactorChallengeControllerTest.php
new file mode 100644
index 00000000000..d9ea1ca263f
--- /dev/null
+++ b/tests/Core/Controller/TwoFactorChallengeControllerTest.php
@@ -0,0 +1,446 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Test\Core\Controller;
+
+use OC\Authentication\TwoFactorAuth\Manager;
+use OC\Authentication\TwoFactorAuth\ProviderSet;
+use OC\Core\Controller\TwoFactorChallengeController;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\AppFramework\Http\StandaloneTemplateResponse;
+use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin;
+use OCP\Authentication\TwoFactorAuth\ILoginSetupProvider;
+use OCP\Authentication\TwoFactorAuth\IProvider;
+use OCP\Authentication\TwoFactorAuth\TwoFactorException;
+use OCP\IRequest;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use OCP\Template\ITemplate;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+class TwoFactorChallengeControllerTest extends TestCase {
+ /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */
+ private $request;
+
+ /** @var Manager|\PHPUnit\Framework\MockObject\MockObject */
+ private $twoFactorManager;
+
+ /** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
+ private $userSession;
+
+ /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */
+ private $session;
+
+ /** @var IURLGenerator|\PHPUnit\Framework\MockObject\MockObject */
+ private $urlGenerator;
+
+ /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */
+ private $logger;
+
+ /** @var TwoFactorChallengeController|\PHPUnit\Framework\MockObject\MockObject */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->twoFactorManager = $this->createMock(Manager::class);
+ $this->userSession = $this->createMock(IUserSession::class);
+ $this->session = $this->createMock(ISession::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->controller = $this->getMockBuilder(TwoFactorChallengeController::class)
+ ->setConstructorArgs([
+ 'core',
+ $this->request,
+ $this->twoFactorManager,
+ $this->userSession,
+ $this->session,
+ $this->urlGenerator,
+ $this->logger,
+ ])
+ ->onlyMethods(['getLogoutUrl'])
+ ->getMock();
+ $this->controller->expects($this->any())
+ ->method('getLogoutUrl')
+ ->willReturn('logoutAttribute');
+ }
+
+ public function testSelectChallenge(): void {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+ $p1 = $this->createMock(IActivatableAtLogin::class);
+ $p1->method('getId')->willReturn('p1');
+ $backupProvider = $this->createMock(IProvider::class);
+ $backupProvider->method('getId')->willReturn('backup_codes');
+ $providerSet = new ProviderSet([$p1, $backupProvider], true);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getLoginSetupProviders')
+ ->with($user)
+ ->willReturn([$p1]);
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProviderSet')
+ ->with($user)
+ ->willReturn($providerSet);
+
+ $expected = new StandaloneTemplateResponse('core', 'twofactorselectchallenge', [
+ 'providers' => [
+ $p1,
+ ],
+ 'providerMissing' => true,
+ 'backupProvider' => $backupProvider,
+ 'redirect_url' => '/some/url',
+ 'logout_url' => 'logoutAttribute',
+ 'hasSetupProviders' => true,
+ ], 'guest');
+
+ $this->assertEquals($expected, $this->controller->selectChallenge('/some/url'));
+ }
+
+ public function testShowChallenge(): void {
+ $user = $this->createMock(IUser::class);
+ $provider = $this->createMock(IProvider::class);
+ $provider->method('getId')->willReturn('myprovider');
+ $backupProvider = $this->createMock(IProvider::class);
+ $backupProvider->method('getId')->willReturn('backup_codes');
+ $tmpl = $this->createMock(ITemplate::class);
+ $providerSet = new ProviderSet([$provider, $backupProvider], true);
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProviderSet')
+ ->with($user)
+ ->willReturn($providerSet);
+ $provider->expects($this->once())
+ ->method('getId')
+ ->willReturn('u2f');
+ $backupProvider->expects($this->once())
+ ->method('getId')
+ ->willReturn('backup_codes');
+
+ $this->session->expects($this->once())
+ ->method('exists')
+ ->with('two_factor_auth_error')
+ ->willReturn(true);
+ $this->session->expects($this->exactly(2))
+ ->method('remove')
+ ->with($this->logicalOr($this->equalTo('two_factor_auth_error'), $this->equalTo('two_factor_auth_error_message')));
+ $provider->expects($this->once())
+ ->method('getTemplate')
+ ->with($user)
+ ->willReturn($tmpl);
+ $tmpl->expects($this->once())
+ ->method('fetchPage')
+ ->willReturn('<html/>');
+
+ $expected = new StandaloneTemplateResponse('core', 'twofactorshowchallenge', [
+ 'error' => true,
+ 'provider' => $provider,
+ 'backupProvider' => $backupProvider,
+ 'logout_url' => 'logoutAttribute',
+ 'template' => '<html/>',
+ 'redirect_url' => '/re/dir/ect/url',
+ 'error_message' => null,
+ ], 'guest');
+
+ $this->assertEquals($expected, $this->controller->showChallenge('myprovider', '/re/dir/ect/url'));
+ }
+
+ public function testShowInvalidChallenge(): void {
+ $user = $this->createMock(IUser::class);
+ $providerSet = new ProviderSet([], false);
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProviderSet')
+ ->with($user)
+ ->willReturn($providerSet);
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.TwoFactorChallenge.selectChallenge')
+ ->willReturn('select/challenge/url');
+
+ $expected = new RedirectResponse('select/challenge/url');
+
+ $this->assertEquals($expected, $this->controller->showChallenge('myprovider', 'redirect/url'));
+ }
+
+ public function testSolveChallenge(): void {
+ $user = $this->createMock(IUser::class);
+ $provider = $this->createMock(IProvider::class);
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProvider')
+ ->with($user, 'myprovider')
+ ->willReturn($provider);
+
+ $this->twoFactorManager->expects($this->once())
+ ->method('verifyChallenge')
+ ->with('myprovider', $user, 'token')
+ ->willReturn(true);
+ $this->urlGenerator
+ ->expects($this->once())
+ ->method('linkToDefaultPageUrl')
+ ->willReturn('/default/foo');
+
+ $expected = new RedirectResponse('/default/foo');
+ $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token'));
+ }
+
+ public function testSolveValidChallengeAndRedirect(): void {
+ $user = $this->createMock(IUser::class);
+ $provider = $this->createMock(IProvider::class);
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProvider')
+ ->with($user, 'myprovider')
+ ->willReturn($provider);
+
+ $this->twoFactorManager->expects($this->once())
+ ->method('verifyChallenge')
+ ->with('myprovider', $user, 'token')
+ ->willReturn(true);
+ $this->urlGenerator->expects($this->once())
+ ->method('getAbsoluteURL')
+ ->with('redirect url')
+ ->willReturn('redirect/url');
+
+ $expected = new RedirectResponse('redirect/url');
+ $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token', 'redirect%20url'));
+ }
+
+ public function testSolveChallengeInvalidProvider(): void {
+ $user = $this->getMockBuilder(IUser::class)->getMock();
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProvider')
+ ->with($user, 'myprovider')
+ ->willReturn(null);
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.TwoFactorChallenge.selectChallenge')
+ ->willReturn('select/challenge/url');
+
+ $expected = new RedirectResponse('select/challenge/url');
+
+ $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token'));
+ }
+
+ public function testSolveInvalidChallenge(): void {
+ $user = $this->createMock(IUser::class);
+ $provider = $this->createMock(IProvider::class);
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProvider')
+ ->with($user, 'myprovider')
+ ->willReturn($provider);
+
+ $this->twoFactorManager->expects($this->once())
+ ->method('verifyChallenge')
+ ->with('myprovider', $user, 'token')
+ ->willReturn(false);
+ $this->session->expects($this->once())
+ ->method('set')
+ ->with('two_factor_auth_error', true);
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.TwoFactorChallenge.showChallenge', [
+ 'challengeProviderId' => 'myprovider',
+ 'redirect_url' => '/url',
+ ])
+ ->willReturn('files/index/url');
+ $provider->expects($this->once())
+ ->method('getId')
+ ->willReturn('myprovider');
+
+ $expected = new RedirectResponse('files/index/url');
+ $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token', '/url'));
+ }
+
+ public function testSolveChallengeTwoFactorException(): void {
+ $user = $this->createMock(IUser::class);
+ $provider = $this->createMock(IProvider::class);
+ $exception = new TwoFactorException('2FA failed');
+
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getProvider')
+ ->with($user, 'myprovider')
+ ->willReturn($provider);
+
+ $this->twoFactorManager->expects($this->once())
+ ->method('verifyChallenge')
+ ->with('myprovider', $user, 'token')
+ ->willThrowException($exception);
+ $calls = [
+ ['two_factor_auth_error_message', '2FA failed'],
+ ['two_factor_auth_error', true],
+ ];
+ $this->session->expects($this->exactly(2))
+ ->method('set')
+ ->willReturnCallback(function () use (&$calls): void {
+ $expected = array_shift($calls);
+ $this->assertEquals($expected, func_get_args());
+ });
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.TwoFactorChallenge.showChallenge', [
+ 'challengeProviderId' => 'myprovider',
+ 'redirect_url' => '/url',
+ ])
+ ->willReturn('files/index/url');
+ $provider->expects($this->once())
+ ->method('getId')
+ ->willReturn('myprovider');
+
+ $expected = new RedirectResponse('files/index/url');
+ $this->assertEquals($expected, $this->controller->solveChallenge('myprovider', 'token', '/url'));
+ }
+
+ public function testSetUpProviders(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $provider = $this->createMock(IActivatableAtLogin::class);
+ $this->twoFactorManager->expects($this->once())
+ ->method('getLoginSetupProviders')
+ ->with($user)
+ ->willReturn([
+ $provider,
+ ]);
+ $expected = new StandaloneTemplateResponse(
+ 'core',
+ 'twofactorsetupselection',
+ [
+ 'providers' => [
+ $provider,
+ ],
+ 'logout_url' => 'logoutAttribute',
+ 'redirect_url' => null,
+ ],
+ 'guest'
+ );
+
+ $response = $this->controller->setupProviders();
+
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testSetUpInvalidProvider(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $provider = $this->createMock(IActivatableAtLogin::class);
+ $provider->expects($this->any())
+ ->method('getId')
+ ->willReturn('prov1');
+ $this->twoFactorManager->expects($this->once())
+ ->method('getLoginSetupProviders')
+ ->with($user)
+ ->willReturn([
+ $provider,
+ ]);
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.TwoFactorChallenge.selectChallenge')
+ ->willReturn('2fa/select/page');
+ $expected = new RedirectResponse('2fa/select/page');
+
+ $response = $this->controller->setupProvider('prov2');
+
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testSetUpProvider(): void {
+ $user = $this->createMock(IUser::class);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $provider = $this->createMock(IActivatableAtLogin::class);
+ $provider->expects($this->any())
+ ->method('getId')
+ ->willReturn('prov1');
+ $this->twoFactorManager->expects($this->once())
+ ->method('getLoginSetupProviders')
+ ->with($user)
+ ->willReturn([
+ $provider,
+ ]);
+ $loginSetup = $this->createMock(ILoginSetupProvider::class);
+ $provider->expects($this->any())
+ ->method('getLoginSetup')
+ ->with($user)
+ ->willReturn($loginSetup);
+ $tmpl = $this->createMock(ITemplate::class);
+ $loginSetup->expects($this->once())
+ ->method('getBody')
+ ->willReturn($tmpl);
+ $tmpl->expects($this->once())
+ ->method('fetchPage')
+ ->willReturn('tmpl');
+ $expected = new StandaloneTemplateResponse(
+ 'core',
+ 'twofactorsetupchallenge',
+ [
+ 'provider' => $provider,
+ 'logout_url' => 'logoutAttribute',
+ 'template' => 'tmpl',
+ 'redirect_url' => null,
+ ],
+ 'guest'
+ );
+
+ $response = $this->controller->setupProvider('prov1');
+
+ $this->assertEquals($expected, $response);
+ }
+
+ public function testConfirmProviderSetup(): void {
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with(
+ 'core.TwoFactorChallenge.showChallenge',
+ [
+ 'challengeProviderId' => 'totp',
+ 'redirect_url' => null,
+ ])
+ ->willReturn('2fa/select/page');
+ $expected = new RedirectResponse('2fa/select/page');
+
+ $response = $this->controller->confirmProviderSetup('totp');
+
+ $this->assertEquals($expected, $response);
+ }
+}
diff --git a/tests/Core/Controller/UserControllerTest.php b/tests/Core/Controller/UserControllerTest.php
new file mode 100644
index 00000000000..2473f280580
--- /dev/null
+++ b/tests/Core/Controller/UserControllerTest.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Test\Core\Controller;
+
+use OC\Core\Controller\UserController;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserManager;
+use Test\TestCase;
+
+class UserControllerTest extends TestCase {
+ /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */
+ private $userManager;
+
+ /** @var UserController */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->userManager = $this->createMock(IUserManager::class);
+ $this->controller = new UserController(
+ 'core',
+ $this->createMock(IRequest::class),
+ $this->userManager
+ );
+ }
+
+ public function testGetDisplayNames(): void {
+ $user = $this->createMock(IUser::class);
+ $user->method('getDisplayName')
+ ->willReturn('FooDisplay Name');
+
+ $this->userManager
+ ->method('get')
+ ->willReturnCallback(function ($uid) use ($user) {
+ if ($uid === 'foo') {
+ return $user;
+ }
+ return null;
+ });
+
+ $expected = new JSONResponse([
+ 'users' => [
+ 'foo' => 'FooDisplay Name',
+ 'bar' => 'bar',
+ ],
+ 'status' => 'success'
+ ]);
+
+ $result = $this->controller->getDisplayNames(['foo', 'bar']);
+ $this->assertEquals($expected, $result);
+ }
+}
diff --git a/tests/Core/Controller/WellKnownControllerTest.php b/tests/Core/Controller/WellKnownControllerTest.php
new file mode 100644
index 00000000000..35606dc6384
--- /dev/null
+++ b/tests/Core/Controller/WellKnownControllerTest.php
@@ -0,0 +1,69 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Core\Controller\WellKnownController;
+use OC\Http\WellKnown\RequestManager;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\Http\WellKnown\IResponse;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class WellKnownControllerTest extends TestCase {
+ /** @var IRequest|MockObject */
+ private $request;
+
+ /** @var RequestManager|MockObject */
+ private $manager;
+
+ /** @var WellKnownController */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->request = $this->createMock(IRequest::class);
+ $this->manager = $this->createMock(RequestManager::class);
+
+ $this->controller = new WellKnownController(
+ $this->request,
+ $this->manager,
+ );
+ }
+
+ public function testHandleNotProcessed(): void {
+ $httpResponse = $this->controller->handle('nodeinfo');
+
+ self::assertInstanceOf(JSONResponse::class, $httpResponse);
+ self::assertArrayHasKey('X-NEXTCLOUD-WELL-KNOWN', $httpResponse->getHeaders());
+ }
+
+ public function testHandle(): void {
+ $response = $this->createMock(IResponse::class);
+ $jsonResponse = $this->createMock(JSONResponse::class);
+ $response->expects(self::once())
+ ->method('toHttpResponse')
+ ->willReturn($jsonResponse);
+ $this->manager->expects(self::once())
+ ->method('process')
+ ->with(
+ 'nodeinfo',
+ $this->request
+ )->willReturn($response);
+ $jsonResponse->expects(self::once())
+ ->method('addHeader')
+ ->willReturnSelf();
+
+ $httpResponse = $this->controller->handle('nodeinfo');
+
+ self::assertInstanceOf(JSONResponse::class, $httpResponse);
+ }
+}
diff --git a/tests/Core/Controller/WipeControllerTest.php b/tests/Core/Controller/WipeControllerTest.php
new file mode 100644
index 00000000000..5330eb599e6
--- /dev/null
+++ b/tests/Core/Controller/WipeControllerTest.php
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace Tests\Core\Controller;
+
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Token\RemoteWipe;
+use OC\Core\Controller\WipeController;
+use OCP\AppFramework\Http;
+use OCP\IRequest;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class WipeControllerTest extends TestCase {
+ /** @var RemoteWipe|MockObject */
+ private $remoteWipe;
+
+ /** @var WipeController */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->remoteWipe = $this->createMock(RemoteWipe::class);
+ $this->controller = new WipeController(
+ 'core',
+ $this->createMock(IRequest::class),
+ $this->remoteWipe);
+ }
+
+ public static function dataTest(): array {
+ return [
+ // valid token, could perform operation, valid result
+ [ true, true, true],
+ [ true, false, false],
+ [false, true, false],
+ [false, false, false],
+ ];
+ }
+
+ /**
+ * @param bool $valid
+ * @param bool $couldPerform
+ * @param bool $result
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTest')]
+ public function testCheckWipe(bool $valid, bool $couldPerform, bool $result): void {
+ if (!$valid) {
+ $this->remoteWipe->method('start')
+ ->with('mytoken')
+ ->willThrowException(new InvalidTokenException());
+ } else {
+ $this->remoteWipe->method('start')
+ ->with('mytoken')
+ ->willReturn($couldPerform);
+ }
+
+ $result = $this->controller->checkWipe('mytoken');
+
+ if (!$valid || !$couldPerform) {
+ $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus());
+ $this->assertSame([], $result->getData());
+ } else {
+ $this->assertSame(Http::STATUS_OK, $result->getStatus());
+ $this->assertSame(['wipe' => true], $result->getData());
+ }
+ }
+
+ /**
+ * @param bool $valid
+ * @param bool $couldPerform
+ * @param bool $result
+ */
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataTest')]
+ public function testWipeDone(bool $valid, bool $couldPerform, bool $result): void {
+ if (!$valid) {
+ $this->remoteWipe->method('finish')
+ ->with('mytoken')
+ ->willThrowException(new InvalidTokenException());
+ } else {
+ $this->remoteWipe->method('finish')
+ ->with('mytoken')
+ ->willReturn($couldPerform);
+ }
+
+ $result = $this->controller->wipeDone('mytoken');
+
+ if (!$valid || !$couldPerform) {
+ $this->assertSame(Http::STATUS_NOT_FOUND, $result->getStatus());
+ $this->assertSame([], $result->getData());
+ } else {
+ $this->assertSame(Http::STATUS_OK, $result->getStatus());
+ $this->assertSame([], $result->getData());
+ }
+ }
+}
diff --git a/tests/Core/Data/LoginFlowV2CredentialsTest.php b/tests/Core/Data/LoginFlowV2CredentialsTest.php
new file mode 100644
index 00000000000..ffa06f1a451
--- /dev/null
+++ b/tests/Core/Data/LoginFlowV2CredentialsTest.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Data;
+
+use JsonSerializable;
+use OC\Core\Data\LoginFlowV2Credentials;
+use Test\TestCase;
+
+class LoginFlowV2CredentialsTest extends TestCase {
+ /** @var LoginFlowV2Credentials */
+ private $fixture;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->fixture = new LoginFlowV2Credentials('server', 'login', 'pass');
+ }
+
+ public function testImplementsJsonSerializable(): void {
+ $this->assertTrue($this->fixture instanceof JsonSerializable);
+ }
+
+ /**
+ * Test getter functions.
+ */
+ public function testGetter(): void {
+ $this->assertEquals('server', $this->fixture->getServer());
+ $this->assertEquals('login', $this->fixture->getLoginName());
+ $this->assertEquals('pass', $this->fixture->getAppPassword());
+ }
+
+ public function testJsonSerialize(): void {
+ $this->assertEquals(
+ [
+ 'server' => 'server',
+ 'loginName' => 'login',
+ 'appPassword' => 'pass',
+ ],
+ $this->fixture->jsonSerialize()
+ );
+ }
+}
diff --git a/tests/Core/Middleware/TwoFactorMiddlewareTest.php b/tests/Core/Middleware/TwoFactorMiddlewareTest.php
new file mode 100644
index 00000000000..10afdd7c5e1
--- /dev/null
+++ b/tests/Core/Middleware/TwoFactorMiddlewareTest.php
@@ -0,0 +1,286 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Test\Core\Middleware;
+
+use OC\AppFramework\Http\Request;
+use OC\Authentication\Exceptions\TwoFactorAuthRequiredException;
+use OC\Authentication\Exceptions\UserAlreadyLoggedInException;
+use OC\Authentication\TwoFactorAuth\Manager;
+use OC\Authentication\TwoFactorAuth\ProviderSet;
+use OC\Core\Controller\TwoFactorChallengeController;
+use OC\Core\Middleware\TwoFactorMiddleware;
+use OC\User\Session;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\RedirectResponse;
+use OCP\AppFramework\Utility\IControllerMethodReflector;
+use OCP\Authentication\TwoFactorAuth\ALoginSetupController;
+use OCP\Authentication\TwoFactorAuth\IProvider;
+use OCP\IConfig;
+use OCP\IRequest;
+use OCP\IRequestId;
+use OCP\ISession;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserSession;
+use PHPUnit\Framework\MockObject\MockObject;
+use Test\TestCase;
+
+class TwoFactorMiddlewareTest extends TestCase {
+ /** @var Manager|MockObject */
+ private $twoFactorManager;
+
+ /** @var IUserSession|MockObject */
+ private $userSession;
+
+ /** @var ISession|MockObject */
+ private $session;
+
+ /** @var IURLGenerator|MockObject */
+ private $urlGenerator;
+
+ /** @var IControllerMethodReflector|MockObject */
+ private $reflector;
+
+ /** @var IRequest|MockObject */
+ private $request;
+
+ /** @var TwoFactorMiddleware */
+ private $middleware;
+
+ /** @var Controller */
+ private $controller;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->twoFactorManager = $this->getMockBuilder(Manager::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->userSession = $this->getMockBuilder(Session::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->session = $this->createMock(ISession::class);
+ $this->urlGenerator = $this->createMock(IURLGenerator::class);
+ $this->reflector = $this->createMock(IControllerMethodReflector::class);
+ $this->request = new Request(
+ [
+ 'server' => [
+ 'REQUEST_URI' => 'test/url'
+ ]
+ ],
+ $this->createMock(IRequestId::class),
+ $this->createMock(IConfig::class)
+ );
+
+ $this->middleware = new TwoFactorMiddleware($this->twoFactorManager, $this->userSession, $this->session, $this->urlGenerator, $this->reflector, $this->request);
+ $this->controller = $this->createMock(Controller::class);
+ }
+
+ public function testBeforeControllerNotLoggedIn(): void {
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(false);
+
+ $this->userSession->expects($this->never())
+ ->method('getUser');
+
+ $this->middleware->beforeController($this->controller, 'index');
+ }
+
+ public function testBeforeSetupController(): void {
+ $user = $this->createMock(IUser::class);
+ $controller = $this->createMock(ALoginSetupController::class);
+ $this->userSession->expects($this->any())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('needsSecondFactor')
+ ->willReturn(true);
+ $this->userSession->expects($this->never())
+ ->method('isLoggedIn');
+
+ $this->middleware->beforeController($controller, 'create');
+ }
+
+ public function testBeforeControllerNoTwoFactorCheckNeeded(): void {
+ $user = $this->createMock(IUser::class);
+
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('isTwoFactorAuthenticated')
+ ->with($user)
+ ->willReturn(false);
+
+ $this->middleware->beforeController($this->controller, 'index');
+ }
+
+
+ public function testBeforeControllerTwoFactorAuthRequired(): void {
+ $this->expectException(TwoFactorAuthRequiredException::class);
+
+ $user = $this->createMock(IUser::class);
+
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession->expects($this->once())
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('isTwoFactorAuthenticated')
+ ->with($user)
+ ->willReturn(true);
+ $this->twoFactorManager->expects($this->once())
+ ->method('needsSecondFactor')
+ ->with($user)
+ ->willReturn(true);
+
+ $this->middleware->beforeController($this->controller, 'index');
+ }
+
+
+ public function testBeforeControllerUserAlreadyLoggedIn(): void {
+ $this->expectException(UserAlreadyLoggedInException::class);
+
+ $user = $this->createMock(IUser::class);
+
+ $this->reflector
+ ->method('hasAnnotation')
+ ->willReturn(false);
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('isTwoFactorAuthenticated')
+ ->with($user)
+ ->willReturn(true);
+ $this->twoFactorManager->expects($this->once())
+ ->method('needsSecondFactor')
+ ->with($user)
+ ->willReturn(false);
+
+ $twoFactorChallengeController = $this->getMockBuilder(TwoFactorChallengeController::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->middleware->beforeController($twoFactorChallengeController, 'index');
+ }
+
+ public function testAfterExceptionTwoFactorAuthRequired(): void {
+ $ex = new TwoFactorAuthRequiredException();
+
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('core.TwoFactorChallenge.selectChallenge')
+ ->willReturn('test/url');
+ $expected = new RedirectResponse('test/url');
+
+ $this->assertEquals($expected, $this->middleware->afterException($this->controller, 'index', $ex));
+ }
+
+ public function testAfterException(): void {
+ $ex = new UserAlreadyLoggedInException();
+
+ $this->urlGenerator->expects($this->once())
+ ->method('linkToRoute')
+ ->with('files.view.index')
+ ->willReturn('redirect/url');
+ $expected = new RedirectResponse('redirect/url');
+
+ $this->assertEquals($expected, $this->middleware->afterException($this->controller, 'index', $ex));
+ }
+
+ public function testRequires2FASetupDoneAnnotated(): void {
+ $user = $this->createMock(IUser::class);
+
+ $this->reflector
+ ->method('hasAnnotation')
+ ->willReturnCallback(function (string $annotation) {
+ return $annotation === 'TwoFactorSetUpDoneRequired';
+ });
+ $this->userSession->expects($this->once())
+ ->method('isLoggedIn')
+ ->willReturn(true);
+ $this->userSession
+ ->method('getUser')
+ ->willReturn($user);
+ $this->twoFactorManager->expects($this->once())
+ ->method('isTwoFactorAuthenticated')
+ ->with($user)
+ ->willReturn(true);
+ $this->twoFactorManager->expects($this->once())
+ ->method('needsSecondFactor')
+ ->with($user)
+ ->willReturn(false);
+
+ $this->expectException(UserAlreadyLoggedInException::class);
+
+ $twoFactorChallengeController = $this->getMockBuilder(TwoFactorChallengeController::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->middleware->beforeController($twoFactorChallengeController, 'index');
+ }
+
+ public static function dataRequires2FASetupDone(): array {
+ return [
+ [false, false, false],
+ [false, true, true],
+ [true, false, true],
+ [true, true, true],
+ ];
+ }
+
+ #[\PHPUnit\Framework\Attributes\DataProvider('dataRequires2FASetupDone')]
+ public function testRequires2FASetupDone(bool $hasProvider, bool $missingProviders, bool $expectEception): void {
+ if ($hasProvider) {
+ $provider = $this->createMock(IProvider::class);
+ $provider->method('getId')
+ ->willReturn('2FAftw');
+ $providers = [$provider];
+ } else {
+ $providers = [];
+ }
+
+
+ $user = $this->createMock(IUser::class);
+
+ $this->reflector
+ ->method('hasAnnotation')
+ ->willReturn(false);
+ $this->userSession
+ ->method('getUser')
+ ->willReturn($user);
+ $providerSet = new ProviderSet($providers, $missingProviders);
+ $this->twoFactorManager->method('getProviderSet')
+ ->with($user)
+ ->willReturn($providerSet);
+ $this->userSession
+ ->method('isLoggedIn')
+ ->willReturn(false);
+
+ if ($expectEception) {
+ $this->expectException(TwoFactorAuthRequiredException::class);
+ } else {
+ // hack to make phpunit shut up. Since we don't expect an exception here...
+ $this->assertTrue(true);
+ }
+
+ $twoFactorChallengeController = $this->getMockBuilder(TwoFactorChallengeController::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->middleware->beforeController($twoFactorChallengeController, 'index');
+ }
+}
diff --git a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php
new file mode 100644
index 00000000000..5d05a1c6e0a
--- /dev/null
+++ b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php
@@ -0,0 +1,461 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace Tests\Core\Data;
+
+use Exception;
+use OC\Authentication\Exceptions\InvalidTokenException;
+use OC\Authentication\Token\IProvider;
+use OC\Authentication\Token\IToken;
+use OC\Core\Data\LoginFlowV2Credentials;
+use OC\Core\Data\LoginFlowV2Tokens;
+use OC\Core\Db\LoginFlowV2;
+use OC\Core\Db\LoginFlowV2Mapper;
+use OC\Core\Exception\LoginFlowV2ClientForbiddenException;
+use OC\Core\Exception\LoginFlowV2NotFoundException;
+use OC\Core\Service\LoginFlowV2Service;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\IConfig;
+use OCP\Security\ICrypto;
+use OCP\Security\ISecureRandom;
+use PHPUnit\Framework\MockObject\MockObject;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+
+/**
+ * Unit tests for \OC\Core\Service\LoginFlowV2Service
+ */
+class LoginFlowV2ServiceUnitTest extends TestCase {
+ /** @var IConfig */
+ private $config;
+
+ /** @var ICrypto */
+ private $crypto;
+
+ /** @var LoggerInterface|MockObject */
+ private $logger;
+
+ /** @var LoginFlowV2Mapper */
+ private $mapper;
+
+ /** @var ISecureRandom */
+ private $secureRandom;
+
+ /** @var LoginFlowV2Service */
+ private $subjectUnderTest;
+
+ /** @var ITimeFactory */
+ private $timeFactory;
+
+ /** @var \OC\Authentication\Token\IProvider */
+ private $tokenProvider;
+
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->setupSubjectUnderTest();
+ }
+
+ /**
+ * Setup subject under test with mocked constructor arguments.
+ *
+ * Code was moved to separate function to keep setUp function small and clear.
+ */
+ private function setupSubjectUnderTest(): void {
+ $this->config = $this->createMock(IConfig::class);
+ $this->crypto = $this->createMock(ICrypto::class);
+ $this->mapper = $this->createMock(LoginFlowV2Mapper::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->tokenProvider = $this->createMock(IProvider::class);
+ $this->secureRandom = $this->createMock(ISecureRandom::class);
+ $this->timeFactory = $this->createMock(ITimeFactory::class);
+
+ $this->subjectUnderTest = new LoginFlowV2Service(
+ $this->mapper,
+ $this->secureRandom,
+ $this->timeFactory,
+ $this->config,
+ $this->crypto,
+ $this->logger,
+ $this->tokenProvider
+ );
+ }
+
+ /**
+ * Generates for a given password required OpenSSL parts.
+ *
+ * @return array Array contains encrypted password, private key and public key.
+ */
+ private function getOpenSSLEncryptedPublicAndPrivateKey(string $appPassword): array {
+ // Create the private and public key
+ $res = openssl_pkey_new([
+ 'digest_alg' => 'md5', // take fast algorithm for testing purposes
+ 'private_key_bits' => 512,
+ 'private_key_type' => OPENSSL_KEYTYPE_RSA,
+ ]);
+
+ // Extract the private key from $res
+ openssl_pkey_export($res, $privateKey);
+
+ // Extract the public key from $res
+ $publicKey = openssl_pkey_get_details($res);
+ $publicKey = $publicKey['key'];
+
+ // Encrypt the data to $encrypted using the public key
+ openssl_public_encrypt($appPassword, $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING);
+
+ return [$encrypted, $privateKey, $publicKey];
+ }
+
+ /*
+ * Tests for poll
+ */
+ public function testPollPrivateKeyCouldNotBeDecrypted(): void {
+ $this->expectException(LoginFlowV2NotFoundException::class);
+ $this->expectExceptionMessage('Apptoken could not be decrypted');
+
+ $this->crypto->expects($this->once())
+ ->method('decrypt')
+ ->willThrowException(new \Exception('HMAC mismatch'));
+
+ /*
+ * Cannot be mocked, because functions like getLoginName are magic functions.
+ * To be able to set internal properties, we have to use the real class here.
+ */
+ $loginFlowV2 = new LoginFlowV2();
+ $loginFlowV2->setLoginName('test');
+ $loginFlowV2->setServer('test');
+ $loginFlowV2->setAppPassword('test');
+ $loginFlowV2->setPrivateKey('test');
+
+ $this->mapper->expects($this->once())
+ ->method('getByPollToken')
+ ->willReturn($loginFlowV2);
+
+ $this->subjectUnderTest->poll('');
+ }
+
+ public function testPollApptokenCouldNotBeDecrypted(): void {
+ $this->expectException(LoginFlowV2NotFoundException::class);
+ $this->expectExceptionMessage('Apptoken could not be decrypted');
+
+ /*
+ * Cannot be mocked, because functions like getLoginName are magic functions.
+ * To be able to set internal properties, we have to use the real class here.
+ */
+ [$encrypted, $privateKey,] = $this->getOpenSSLEncryptedPublicAndPrivateKey('test');
+ $loginFlowV2 = new LoginFlowV2();
+ $loginFlowV2->setLoginName('test');
+ $loginFlowV2->setServer('test');
+ $loginFlowV2->setAppPassword('broken#' . $encrypted);
+ $loginFlowV2->setPrivateKey('encrypted(test)');
+
+ $this->crypto->expects($this->once())
+ ->method('decrypt')
+ ->willReturn($privateKey);
+
+ $this->mapper->expects($this->once())
+ ->method('getByPollToken')
+ ->willReturn($loginFlowV2);
+
+ $this->subjectUnderTest->poll('test');
+ }
+
+ public function testPollInvalidToken(): void {
+ $this->expectException(LoginFlowV2NotFoundException::class);
+ $this->expectExceptionMessage('Invalid token');
+
+ $this->mapper->expects($this->once())
+ ->method('getByPollToken')
+ ->willThrowException(new DoesNotExistException(''));
+
+ $this->subjectUnderTest->poll('');
+ }
+
+ public function testPollTokenNotYetReady(): void {
+ $this->expectException(LoginFlowV2NotFoundException::class);
+ $this->expectExceptionMessage('Token not yet ready');
+
+ $this->subjectUnderTest->poll('');
+ }
+
+ public function testPollRemoveDataFromDb(): void {
+ [$encrypted, $privateKey] = $this->getOpenSSLEncryptedPublicAndPrivateKey('test_pass');
+
+ $this->crypto->expects($this->once())
+ ->method('decrypt')
+ ->willReturn($privateKey);
+
+ /*
+ * Cannot be mocked, because functions like getLoginName are magic functions.
+ * To be able to set internal properties, we have to use the real class here.
+ */
+ $loginFlowV2 = new LoginFlowV2();
+ $loginFlowV2->setLoginName('test_login');
+ $loginFlowV2->setServer('test_server');
+ $loginFlowV2->setAppPassword(base64_encode($encrypted));
+ $loginFlowV2->setPrivateKey($privateKey);
+
+ $this->mapper->expects($this->once())
+ ->method('delete')
+ ->with($this->equalTo($loginFlowV2));
+
+ $this->mapper->expects($this->once())
+ ->method('getByPollToken')
+ ->willReturn($loginFlowV2);
+
+ $credentials = $this->subjectUnderTest->poll('');
+
+ $this->assertTrue($credentials instanceof LoginFlowV2Credentials);
+ $this->assertEquals(
+ [
+ 'server' => 'test_server',
+ 'loginName' => 'test_login',
+ 'appPassword' => 'test_pass',
+ ],
+ $credentials->jsonSerialize()
+ );
+ }
+
+ /*
+ * Tests for getByLoginToken
+ */
+
+ public function testGetByLoginToken(): void {
+ $loginFlowV2 = new LoginFlowV2();
+ $loginFlowV2->setLoginName('test_login');
+ $loginFlowV2->setServer('test_server');
+ $loginFlowV2->setAppPassword('test');
+
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willReturn($loginFlowV2);
+
+ $result = $this->subjectUnderTest->getByLoginToken('test_token');
+
+ $this->assertTrue($result instanceof LoginFlowV2);
+ $this->assertEquals('test_server', $result->getServer());
+ $this->assertEquals('test_login', $result->getLoginName());
+ $this->assertEquals('test', $result->getAppPassword());
+ }
+
+ public function testGetByLoginTokenLoginTokenInvalid(): void {
+ $this->expectException(LoginFlowV2NotFoundException::class);
+ $this->expectExceptionMessage('Login token invalid');
+
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willThrowException(new DoesNotExistException(''));
+
+ $this->subjectUnderTest->getByLoginToken('test_token');
+ }
+
+ public function testGetByLoginTokenClientForbidden() {
+ $this->expectException(LoginFlowV2ClientForbiddenException::class);
+ $this->expectExceptionMessage('Client not allowed');
+
+ $allowedClients = [
+ '/Custom Allowed Client/i'
+ ];
+
+ $this->config->expects($this->exactly(1))
+ ->method('getSystemValue')
+ ->willReturn($this->returnCallback(function ($key) use ($allowedClients) {
+ // Note: \OCP\IConfig::getSystemValue returns either an array or string.
+ return $key == 'core.login_flow_v2.allowed_user_agents' ? $allowedClients : '';
+ }));
+
+ $loginFlowV2 = new LoginFlowV2();
+ $loginFlowV2->setClientName('Rogue Curl Client/1.0');
+
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willReturn($loginFlowV2);
+
+ $this->subjectUnderTest->getByLoginToken('test_token');
+ }
+
+ public function testGetByLoginTokenClientAllowed() {
+ $allowedClients = [
+ '/Foo Allowed Client/i',
+ '/Custom Allowed Client/i'
+ ];
+
+ $loginFlowV2 = new LoginFlowV2();
+ $loginFlowV2->setClientName('Custom Allowed Client Curl Client/1.0');
+
+ $this->config->expects($this->exactly(1))
+ ->method('getSystemValue')
+ ->willReturn($this->returnCallback(function ($key) use ($allowedClients) {
+ // Note: \OCP\IConfig::getSystemValue returns either an array or string.
+ return $key == 'core.login_flow_v2.allowed_user_agents' ? $allowedClients : '';
+ }));
+
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willReturn($loginFlowV2);
+
+ $result = $this->subjectUnderTest->getByLoginToken('test_token');
+
+ $this->assertTrue($result instanceof LoginFlowV2);
+ $this->assertEquals('Custom Allowed Client Curl Client/1.0', $result->getClientName());
+ }
+
+ /*
+ * Tests for startLoginFlow
+ */
+
+ public function testStartLoginFlow(): void {
+ $loginFlowV2 = new LoginFlowV2();
+
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willReturn($loginFlowV2);
+
+ $this->mapper->expects($this->once())
+ ->method('update');
+
+ $this->assertTrue($this->subjectUnderTest->startLoginFlow('test_token'));
+ }
+
+ public function testStartLoginFlowDoesNotExistException(): void {
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willThrowException(new DoesNotExistException(''));
+
+ $this->assertFalse($this->subjectUnderTest->startLoginFlow('test_token'));
+ }
+
+ /**
+ * If an exception not of type DoesNotExistException is thrown,
+ * it is expected that it is not being handled by startLoginFlow.
+ */
+ public function testStartLoginFlowException(): void {
+ $this->expectException(Exception::class);
+
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willThrowException(new Exception(''));
+
+ $this->subjectUnderTest->startLoginFlow('test_token');
+ }
+
+ /*
+ * Tests for flowDone
+ */
+
+ public function testFlowDone(): void {
+ [,, $publicKey] = $this->getOpenSSLEncryptedPublicAndPrivateKey('test_pass');
+
+ $loginFlowV2 = new LoginFlowV2();
+ $loginFlowV2->setPublicKey($publicKey);
+ $loginFlowV2->setClientName('client_name');
+
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willReturn($loginFlowV2);
+
+ $this->mapper->expects($this->once())
+ ->method('update');
+
+ $this->secureRandom->expects($this->once())
+ ->method('generate')
+ ->with(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS)
+ ->willReturn('test_pass');
+
+ // session token
+ $sessionToken = $this->getMockBuilder(IToken::class)->disableOriginalConstructor()->getMock();
+ $sessionToken->expects($this->once())
+ ->method('getLoginName')
+ ->willReturn('login_name');
+
+ $this->tokenProvider->expects($this->once())
+ ->method('getPassword')
+ ->willReturn('test_pass');
+
+ $this->tokenProvider->expects($this->once())
+ ->method('getToken')
+ ->willReturn($sessionToken);
+
+ $this->tokenProvider->expects($this->once())
+ ->method('generateToken')
+ ->with(
+ 'test_pass',
+ 'user_id',
+ 'login_name',
+ 'test_pass',
+ 'client_name',
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER
+ );
+
+ $result = $this->subjectUnderTest->flowDone(
+ 'login_token',
+ 'session_id',
+ 'server',
+ 'user_id'
+ );
+ $this->assertTrue($result);
+
+ // app password is encrypted and must look like:
+ // ZACZOOzxTpKz4+KXL5kZ/gCK0xvkaVi/8yzupAn6Ui6+5qCSKvfPKGgeDRKs0sivvSLzk/XSp811SZCZmH0Y3g==
+ $this->assertMatchesRegularExpression('/[a-zA-Z\/0-9+=]+/', $loginFlowV2->getAppPassword());
+
+ $this->assertEquals('server', $loginFlowV2->getServer());
+ }
+
+ public function testFlowDoneDoesNotExistException(): void {
+ $this->mapper->expects($this->once())
+ ->method('getByLoginToken')
+ ->willThrowException(new DoesNotExistException(''));
+
+ $result = $this->subjectUnderTest->flowDone(
+ 'login_token',
+ 'session_id',
+ 'server',
+ 'user_id'
+ );
+ $this->assertFalse($result);
+ }
+
+ public function testFlowDonePasswordlessTokenException(): void {
+ $this->tokenProvider->expects($this->once())
+ ->method('getToken')
+ ->willThrowException(new InvalidTokenException(''));
+
+ $result = $this->subjectUnderTest->flowDone(
+ 'login_token',
+ 'session_id',
+ 'server',
+ 'user_id'
+ );
+ $this->assertFalse($result);
+ }
+
+ /*
+ * Tests for createTokens
+ */
+
+ public function testCreateTokens(): void {
+ $this->config->expects($this->exactly(2))
+ ->method('getSystemValue')
+ ->willReturn($this->returnCallback(function ($key) {
+ // Note: \OCP\IConfig::getSystemValue returns either an array or string.
+ return $key == 'openssl' ? [] : '';
+ }));
+
+ $this->mapper->expects($this->once())
+ ->method('insert');
+
+ $this->secureRandom->expects($this->exactly(2))
+ ->method('generate');
+
+ $token = $this->subjectUnderTest->createTokens('user_agent');
+ $this->assertTrue($token instanceof LoginFlowV2Tokens);
+ }
+}