diff options
Diffstat (limited to 'tests/Core')
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); + } +} |