diff options
Diffstat (limited to 'tests/lib/User')
-rw-r--r-- | tests/lib/User/AvailabilityCoordinatorTest.php | 220 | ||||
-rw-r--r-- | tests/lib/User/AvatarUserDummy.php | 15 | ||||
-rw-r--r-- | tests/lib/User/Backend.php | 111 | ||||
-rw-r--r-- | tests/lib/User/DatabaseTest.php | 157 | ||||
-rw-r--r-- | tests/lib/User/Dummy.php | 16 | ||||
-rw-r--r-- | tests/lib/User/ManagerTest.php | 807 | ||||
-rw-r--r-- | tests/lib/User/SessionTest.php | 1324 | ||||
-rw-r--r-- | tests/lib/User/UserTest.php | 1032 |
8 files changed, 3682 insertions, 0 deletions
diff --git a/tests/lib/User/AvailabilityCoordinatorTest.php b/tests/lib/User/AvailabilityCoordinatorTest.php new file mode 100644 index 00000000000..09c1528912b --- /dev/null +++ b/tests/lib/User/AvailabilityCoordinatorTest.php @@ -0,0 +1,220 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\User; + +use OC\User\AvailabilityCoordinator; +use OC\User\OutOfOfficeData; +use OCA\DAV\CalDAV\TimezoneService; +use OCA\DAV\Db\Absence; +use OCA\DAV\Service\AbsenceService; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class AvailabilityCoordinatorTest extends TestCase { + private AvailabilityCoordinator $availabilityCoordinator; + private ICacheFactory $cacheFactory; + private ICache $cache; + private IConfig|MockObject $config; + private AbsenceService $absenceService; + private LoggerInterface $logger; + private MockObject|TimezoneService $timezoneService; + + protected function setUp(): void { + parent::setUp(); + + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->cache = $this->createMock(ICache::class); + $this->absenceService = $this->createMock(AbsenceService::class); + $this->config = $this->createMock(IConfig::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->timezoneService = $this->createMock(TimezoneService::class); + + $this->cacheFactory->expects(self::once()) + ->method('createLocal') + ->willReturn($this->cache); + + $this->availabilityCoordinator = new AvailabilityCoordinator( + $this->cacheFactory, + $this->config, + $this->absenceService, + $this->logger, + $this->timezoneService, + ); + } + + public function testIsEnabled(): void { + $this->config->expects(self::once()) + ->method('getAppValue') + ->with('dav', 'hide_absence_settings', 'no') + ->willReturn('no'); + + $isEnabled = $this->availabilityCoordinator->isEnabled(); + + self::assertTrue($isEnabled); + } + + public function testGetOutOfOfficeDataInEffect(): void { + $absence = new Absence(); + $absence->setId(420); + $absence->setUserId('user'); + $absence->setFirstDay('2023-10-01'); + $absence->setLastDay('2023-10-08'); + $absence->setStatus('Vacation'); + $absence->setMessage('On vacation'); + $absence->setReplacementUserId('batman'); + $absence->setReplacementUserDisplayName('Bruce Wayne'); + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->cache->expects(self::exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls(null, null); + $this->absenceService->expects(self::once()) + ->method('getAbsence') + ->with($user->getUID()) + ->willReturn($absence); + + $calls = [ + [$user->getUID() . '_timezone', 'Europe/Berlin', 3600], + [$user->getUID(), '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation","replacementUserId":"batman","replacementUserDisplayName":"Bruce Wayne"}', 300], + ]; + $this->cache->expects(self::exactly(2)) + ->method('set') + ->willReturnCallback(static function () use (&$calls): void { + $expected = array_shift($calls); + self::assertEquals($expected, func_get_args()); + }); + + $expected = new OutOfOfficeData( + '420', + $user, + 1696111200, + 1696802340, + 'Vacation', + 'On vacation', + 'batman', + 'Bruce Wayne', + ); + $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); + self::assertEquals($expected, $actual); + } + + public function testGetOutOfOfficeDataCachedAll(): void { + $absence = new Absence(); + $absence->setId(420); + $absence->setUserId('user'); + $absence->setFirstDay('2023-10-01'); + $absence->setLastDay('2023-10-08'); + $absence->setStatus('Vacation'); + $absence->setMessage('On vacation'); + $absence->setReplacementUserId('batman'); + $absence->setReplacementUserDisplayName('Bruce Wayne'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->cache->expects(self::exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls('UTC', '{"id":"420","startDate":1696118400,"endDate":1696809540,"shortMessage":"Vacation","message":"On vacation","replacementUserId":"batman","replacementUserDisplayName":"Bruce Wayne"}'); + $this->absenceService->expects(self::never()) + ->method('getAbsence'); + $this->cache->expects(self::exactly(1)) + ->method('set'); + + $expected = new OutOfOfficeData( + '420', + $user, + 1696118400, + 1696809540, + 'Vacation', + 'On vacation', + 'batman', + 'Bruce Wayne' + ); + $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); + self::assertEquals($expected, $actual); + } + + public function testGetOutOfOfficeDataNoData(): void { + $absence = new Absence(); + $absence->setId(420); + $absence->setUserId('user'); + $absence->setFirstDay('2023-10-01'); + $absence->setLastDay('2023-10-08'); + $absence->setStatus('Vacation'); + $absence->setMessage('On vacation'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->cache->expects(self::exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls('UTC', null); + $this->absenceService->expects(self::once()) + ->method('getAbsence') + ->willReturn(null); + $this->cache->expects(self::never()) + ->method('set'); + + $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); + self::assertNull($actual); + } + + public function testGetOutOfOfficeDataWithInvalidCachedData(): void { + $absence = new Absence(); + $absence->setId(420); + $absence->setUserId('user'); + $absence->setFirstDay('2023-10-01'); + $absence->setLastDay('2023-10-08'); + $absence->setStatus('Vacation'); + $absence->setMessage('On vacation'); + $absence->setReplacementUserId('batman'); + $absence->setReplacementUserDisplayName('Bruce Wayne'); + $this->timezoneService->method('getUserTimezone')->with('user')->willReturn('Europe/Berlin'); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->cache->expects(self::exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls('UTC', '{"id":"420",}'); + $this->absenceService->expects(self::once()) + ->method('getAbsence') + ->with('user') + ->willReturn($absence); + $this->cache->expects(self::once()) + ->method('set') + ->with('user', '{"id":"420","startDate":1696118400,"endDate":1696809540,"shortMessage":"Vacation","message":"On vacation","replacementUserId":"batman","replacementUserDisplayName":"Bruce Wayne"}', 300); + + $expected = new OutOfOfficeData( + '420', + $user, + 1696118400, + 1696809540, + 'Vacation', + 'On vacation', + 'batman', + 'Bruce Wayne' + ); + $actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user); + self::assertEquals($expected, $actual); + } +} diff --git a/tests/lib/User/AvatarUserDummy.php b/tests/lib/User/AvatarUserDummy.php new file mode 100644 index 00000000000..001dabd24c6 --- /dev/null +++ b/tests/lib/User/AvatarUserDummy.php @@ -0,0 +1,15 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2020-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\User; + +class AvatarUserDummy extends \Test\Util\User\Dummy { + public function canChangeAvatar($uid) { + return true; + } +} diff --git a/tests/lib/User/Backend.php b/tests/lib/User/Backend.php new file mode 100644 index 00000000000..dc5b245fa06 --- /dev/null +++ b/tests/lib/User/Backend.php @@ -0,0 +1,111 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\User; + +/** + * Abstract class to provide the basis of backend-specific unit test classes. + * + * All subclasses MUST assign a backend property in setUp() which implements + * user operations (add, remove, etc.). Test methods in this class will then be + * run on each separate subclass and backend therein. + * + * For an example see /tests/lib/user/dummy.php + */ + +abstract class Backend extends \Test\TestCase { + /** + * @var \OC\User\Backend $backend + */ + protected $backend; + + /** + * get a new unique user name + * test cases can override this in order to clean up created user + * @return string + */ + public function getUser() { + return $this->getUniqueID('test_'); + } + + public function testAddRemove(): void { + //get the number of groups we start with, in case there are exising groups + $startCount = count($this->backend->getUsers()); + + $name1 = $this->getUser(); + $name2 = $this->getUser(); + $this->backend->createUser($name1, ''); + $count = count($this->backend->getUsers()) - $startCount; + $this->assertEquals(1, $count); + $this->assertTrue((array_search($name1, $this->backend->getUsers()) !== false)); + $this->assertFalse((array_search($name2, $this->backend->getUsers()) !== false)); + $this->backend->createUser($name2, ''); + $count = count($this->backend->getUsers()) - $startCount; + $this->assertEquals(2, $count); + $this->assertTrue((array_search($name1, $this->backend->getUsers()) !== false)); + $this->assertTrue((array_search($name2, $this->backend->getUsers()) !== false)); + + $this->backend->deleteUser($name2); + $count = count($this->backend->getUsers()) - $startCount; + $this->assertEquals(1, $count); + $this->assertTrue((array_search($name1, $this->backend->getUsers()) !== false)); + $this->assertFalse((array_search($name2, $this->backend->getUsers()) !== false)); + } + + public function testLogin(): void { + $name1 = $this->getUser(); + $name2 = $this->getUser(); + + $this->assertFalse($this->backend->userExists($name1)); + $this->assertFalse($this->backend->userExists($name2)); + + $this->backend->createUser($name1, 'pass1'); + $this->backend->createUser($name2, 'pass2'); + + $this->assertTrue($this->backend->userExists($name1)); + $this->assertTrue($this->backend->userExists($name2)); + + $this->assertSame($name1, $this->backend->checkPassword($name1, 'pass1')); + $this->assertSame($name2, $this->backend->checkPassword($name2, 'pass2')); + + $this->assertFalse($this->backend->checkPassword($name1, 'pass2')); + $this->assertFalse($this->backend->checkPassword($name2, 'pass1')); + + $this->assertFalse($this->backend->checkPassword($name1, 'dummy')); + $this->assertFalse($this->backend->checkPassword($name2, 'foobar')); + + $this->backend->setPassword($name1, 'newpass1'); + $this->assertFalse($this->backend->checkPassword($name1, 'pass1')); + $this->assertSame($name1, $this->backend->checkPassword($name1, 'newpass1')); + $this->assertFalse($this->backend->checkPassword($name2, 'newpass1')); + } + + public function testSearch(): void { + $name1 = 'foobarbaz'; + $name2 = 'bazbarfoo'; + $name3 = 'notme'; + $name4 = 'under_score'; + + $this->backend->createUser($name1, 'pass1'); + $this->backend->createUser($name2, 'pass2'); + $this->backend->createUser($name3, 'pass3'); + $this->backend->createUser($name4, 'pass4'); + + $result = $this->backend->getUsers('bar'); + $this->assertCount(2, $result); + + $result = $this->backend->getDisplayNames('bar'); + $this->assertCount(2, $result); + + $result = $this->backend->getUsers('under_'); + $this->assertCount(1, $result); + + $result = $this->backend->getUsers('not_'); + $this->assertCount(0, $result); + } +} diff --git a/tests/lib/User/DatabaseTest.php b/tests/lib/User/DatabaseTest.php new file mode 100644 index 00000000000..33101173c0a --- /dev/null +++ b/tests/lib/User/DatabaseTest.php @@ -0,0 +1,157 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\User; + +use OC\User\Database; +use OC\User\User; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\HintException; +use OCP\Security\Events\ValidatePasswordPolicyEvent; +use PHPUnit\Framework\MockObject\MockObject; + +/** + * Class DatabaseTest + * + * @group DB + */ +class DatabaseTest extends Backend { + /** @var array */ + private $users; + /** @var IEventDispatcher|MockObject */ + private $eventDispatcher; + + /** @var \OC\User\Database */ + protected $backend; + + public function getUser() { + $user = parent::getUser(); + $this->users[] = $user; + return $user; + } + + protected function setUp(): void { + parent::setUp(); + + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + + $this->backend = new Database($this->eventDispatcher); + } + + protected function tearDown(): void { + if (!isset($this->users)) { + return; + } + foreach ($this->users as $user) { + $this->backend->deleteUser($user); + } + parent::tearDown(); + } + + public function testVerifyPasswordEvent(): void { + $user = $this->getUser(); + $this->backend->createUser($user, 'pass1'); + + $this->eventDispatcher->expects($this->once())->method('dispatchTyped') + ->willReturnCallback( + function (Event $event): void { + $this->assertInstanceOf(ValidatePasswordPolicyEvent::class, $event); + /** @var ValidatePasswordPolicyEvent $event */ + $this->assertSame('newpass', $event->getPassword()); + } + ); + + $this->backend->setPassword($user, 'newpass'); + $this->assertSame($user, $this->backend->checkPassword($user, 'newpass')); + } + + + public function testVerifyPasswordEventFail(): void { + $this->expectException(HintException::class); + $this->expectExceptionMessage('password change failed'); + + $user = $this->getUser(); + $this->backend->createUser($user, 'pass1'); + + $this->eventDispatcher->expects($this->once())->method('dispatchTyped') + ->willReturnCallback( + function (Event $event): void { + $this->assertInstanceOf(ValidatePasswordPolicyEvent::class, $event); + /** @var ValidatePasswordPolicyEvent $event */ + $this->assertSame('newpass', $event->getPassword()); + throw new HintException('password change failed', 'password change failed'); + } + ); + + $this->backend->setPassword($user, 'newpass'); + $this->assertSame($user, $this->backend->checkPassword($user, 'newpass')); + } + + public function testCreateUserInvalidatesCache(): void { + $user1 = $this->getUniqueID('test_'); + $this->assertFalse($this->backend->userExists($user1)); + $this->backend->createUser($user1, 'pw'); + $this->assertTrue($this->backend->userExists($user1)); + } + + public function testDeleteUserInvalidatesCache(): void { + $user1 = $this->getUniqueID('test_'); + $this->backend->createUser($user1, 'pw'); + $this->assertTrue($this->backend->userExists($user1)); + $this->backend->deleteUser($user1); + $this->assertFalse($this->backend->userExists($user1)); + $this->backend->createUser($user1, 'pw2'); + $this->assertTrue($this->backend->userExists($user1)); + } + + public function testSearch(): void { + parent::testSearch(); + + $user1 = $this->getUser(); + $this->backend->createUser($user1, 'pass1'); + + $user2 = $this->getUser(); + $this->backend->createUser($user2, 'pass1'); + + $user1Obj = new User($user1, $this->backend, $this->createMock(IEventDispatcher::class)); + $user2Obj = new User($user2, $this->backend, $this->createMock(IEventDispatcher::class)); + $emailAddr1 = "$user1@nextcloud.com"; + $emailAddr2 = "$user2@nextcloud.com"; + + $user1Obj->setDisplayName('User 1 Display'); + + $result = $this->backend->getDisplayNames('display'); + $this->assertCount(1, $result); + + $result = $this->backend->getDisplayNames(strtoupper($user1)); + $this->assertCount(1, $result); + + $user1Obj->setEMailAddress($emailAddr1); + $user2Obj->setEMailAddress($emailAddr2); + + $result = $this->backend->getUsers('@nextcloud.com'); + $this->assertCount(2, $result); + + $result = $this->backend->getDisplayNames('@nextcloud.com'); + $this->assertCount(2, $result); + + $result = $this->backend->getDisplayNames('@nextcloud.COM'); + $this->assertCount(2, $result); + } + + public function testUserCount(): void { + $base = $this->backend->countUsers() ?: 0; + $users = $this->backend->getUsers(); + self::assertEquals($base, count($users)); + + $user = $this->getUser(); + $this->backend->createUser($user, $user); + self::assertEquals($base + 1, $this->backend->countUsers()); + } +} diff --git a/tests/lib/User/Dummy.php b/tests/lib/User/Dummy.php new file mode 100644 index 00000000000..ec5be8ec60a --- /dev/null +++ b/tests/lib/User/Dummy.php @@ -0,0 +1,16 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\User; + +class Dummy extends Backend { + protected function setUp(): void { + parent::setUp(); + $this->backend = new \Test\Util\User\Dummy(); + } +} diff --git a/tests/lib/User/ManagerTest.php b/tests/lib/User/ManagerTest.php new file mode 100644 index 00000000000..d5872787d0a --- /dev/null +++ b/tests/lib/User/ManagerTest.php @@ -0,0 +1,807 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\User; + +use OC\AllConfig; +use OC\USER\BACKEND; +use OC\User\Database; +use OC\User\Manager; +use OC\User\User; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ICache; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Server; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +/** + * Class ManagerTest + * + * @group DB + * + * @package Test\User + */ +class ManagerTest extends TestCase { + /** @var IConfig */ + private $config; + /** @var IEventDispatcher */ + private $eventDispatcher; + /** @var ICacheFactory */ + private $cacheFactory; + /** @var ICache */ + private $cache; + /** @var LoggerInterface */ + private $logger; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->cache = $this->createMock(ICache::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->cacheFactory->method('createDistributed') + ->willReturn($this->cache); + } + + public function testGetBackends(): void { + $userDummyBackend = $this->createMock(\Test\Util\User\Dummy::class); + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($userDummyBackend); + $this->assertEquals([$userDummyBackend], $manager->getBackends()); + $dummyDatabaseBackend = $this->createMock(Database::class); + $manager->registerBackend($dummyDatabaseBackend); + $this->assertEquals([$userDummyBackend, $dummyDatabaseBackend], $manager->getBackends()); + } + + + public function testUserExistsSingleBackendExists(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(true); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertTrue($manager->userExists('foo')); + } + + public function testUserExistsTooLong(): void { + /** @var \Test\Util\User\Dummy|MockObject $backend */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->never()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(true); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertFalse($manager->userExists('foo' . str_repeat('a', 62))); + } + + public function testUserExistsSingleBackendNotExists(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(false); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertFalse($manager->userExists('foo')); + } + + public function testUserExistsNoBackends(): void { + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + + $this->assertFalse($manager->userExists('foo')); + } + + public function testUserExistsTwoBackendsSecondExists(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend1 + */ + $backend1 = $this->createMock(\Test\Util\User\Dummy::class); + $backend1->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(false); + + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend2 + */ + $backend2 = $this->createMock(\Test\Util\User\Dummy::class); + $backend2->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(true); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend1); + $manager->registerBackend($backend2); + + $this->assertTrue($manager->userExists('foo')); + } + + public function testUserExistsTwoBackendsFirstExists(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend1 + */ + $backend1 = $this->createMock(\Test\Util\User\Dummy::class); + $backend1->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(true); + + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend2 + */ + $backend2 = $this->createMock(\Test\Util\User\Dummy::class); + $backend2->expects($this->never()) + ->method('userExists'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend1); + $manager->registerBackend($backend2); + + $this->assertTrue($manager->userExists('foo')); + } + + public function testCheckPassword(): void { + /** + * @var \OC\User\Backend | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('checkPassword') + ->with($this->equalTo('foo'), $this->equalTo('bar')) + ->willReturn(true); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === BACKEND::CHECK_PASSWORD) { + return true; + } else { + return false; + } + }); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $user = $manager->checkPassword('foo', 'bar'); + $this->assertTrue($user instanceof User); + } + + public function testCheckPasswordNotSupported(): void { + /** + * @var \OC\User\Backend | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->never()) + ->method('checkPassword'); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(false); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertFalse($manager->checkPassword('foo', 'bar')); + } + + public function testGetOneBackendExists(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(true); + $backend->expects($this->never()) + ->method('loginName2UserName'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertEquals('foo', $manager->get('foo')->getUID()); + } + + public function testGetOneBackendNotExists(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(false); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertEquals(null, $manager->get('foo')); + } + + public function testGetTooLong(): void { + /** @var \Test\Util\User\Dummy|MockObject $backend */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->never()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(false); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertEquals(null, $manager->get('foo' . str_repeat('a', 62))); + } + + public function testGetOneBackendDoNotTranslateLoginNames(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('bLeNdEr')) + ->willReturn(true); + $backend->expects($this->never()) + ->method('loginName2UserName'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertEquals('bLeNdEr', $manager->get('bLeNdEr')->getUID()); + } + + public function testSearchOneBackend(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('getUsers') + ->with($this->equalTo('fo')) + ->willReturn(['foo', 'afoo', 'Afoo1', 'Bfoo']); + $backend->expects($this->never()) + ->method('loginName2UserName'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $result = $manager->search('fo'); + $this->assertEquals(4, count($result)); + $this->assertEquals('afoo', array_shift($result)->getUID()); + $this->assertEquals('Afoo1', array_shift($result)->getUID()); + $this->assertEquals('Bfoo', array_shift($result)->getUID()); + $this->assertEquals('foo', array_shift($result)->getUID()); + } + + public function testSearchTwoBackendLimitOffset(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend1 + */ + $backend1 = $this->createMock(\Test\Util\User\Dummy::class); + $backend1->expects($this->once()) + ->method('getUsers') + ->with($this->equalTo('fo'), $this->equalTo(3), $this->equalTo(1)) + ->willReturn(['foo1', 'foo2']); + $backend1->expects($this->never()) + ->method('loginName2UserName'); + + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend2 + */ + $backend2 = $this->createMock(\Test\Util\User\Dummy::class); + $backend2->expects($this->once()) + ->method('getUsers') + ->with($this->equalTo('fo'), $this->equalTo(3), $this->equalTo(1)) + ->willReturn(['foo3']); + $backend2->expects($this->never()) + ->method('loginName2UserName'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend1); + $manager->registerBackend($backend2); + + $result = $manager->search('fo', 3, 1); + $this->assertEquals(3, count($result)); + $this->assertEquals('foo1', array_shift($result)->getUID()); + $this->assertEquals('foo2', array_shift($result)->getUID()); + $this->assertEquals('foo3', array_shift($result)->getUID()); + } + + public static function dataCreateUserInvalid(): array { + return [ + ['te?st', 'foo', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ["te\tst", '', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ["te\nst", '', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ["te\rst", '', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ["te\0st", '', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ["te\x0Bst", '', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ["te\xe2st", '', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ["te\x80st", '', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ["te\x8bst", '', 'Only the following characters are allowed in a username:' + . ' "a-z", "A-Z", "0-9", spaces and "_.@-\'"'], + ['', 'foo', 'A valid username must be provided'], + [' ', 'foo', 'A valid username must be provided'], + [' test', 'foo', 'Username contains whitespace at the beginning or at the end'], + ['test ', 'foo', 'Username contains whitespace at the beginning or at the end'], + ['.', 'foo', 'Username must not consist of dots only'], + ['..', 'foo', 'Username must not consist of dots only'], + ['.test', '', 'A valid password must be provided'], + ['test', '', 'A valid password must be provided'], + ['test' . str_repeat('a', 61), '', 'Login is too long'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataCreateUserInvalid')] + public function testCreateUserInvalid($uid, $password, $exception): void { + /** @var \Test\Util\User\Dummy|\PHPUnit\Framework\MockObject\MockObject $backend */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('implementsActions') + ->with(\OC\User\Backend::CREATE_USER) + ->willReturn(true); + + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->expectException(\InvalidArgumentException::class, $exception); + $manager->createUser($uid, $password); + } + + public function testCreateUserSingleBackendNotExists(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(true); + + $backend->expects($this->once()) + ->method('createUser') + ->with($this->equalTo('foo'), $this->equalTo('bar')); + + $backend->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(false); + $backend->expects($this->never()) + ->method('loginName2UserName'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $user = $manager->createUser('foo', 'bar'); + $this->assertEquals('foo', $user->getUID()); + } + + + public function testCreateUserSingleBackendExists(): void { + $this->expectException(\Exception::class); + + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(true); + + $backend->expects($this->never()) + ->method('createUser'); + + $backend->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(true); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $manager->createUser('foo', 'bar'); + } + + public function testCreateUserSingleBackendNotSupported(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(false); + + $backend->expects($this->never()) + ->method('createUser'); + + $backend->expects($this->never()) + ->method('userExists'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $this->assertFalse($manager->createUser('foo', 'bar')); + } + + public function testCreateUserNoBackends(): void { + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + + $this->assertFalse($manager->createUser('foo', 'bar')); + } + + + public function testCreateUserFromBackendWithBackendError(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Could not create account'); + + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject $config */ + $config = $this->createMock(IConfig::class); + /** @var \Test\Util\User\Dummy|\PHPUnit\Framework\MockObject\MockObject $backend */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend + ->expects($this->once()) + ->method('createUser') + ->with('MyUid', 'MyPassword') + ->willReturn(false); + + $manager = new Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->createUserFromBackend('MyUid', 'MyPassword', $backend); + } + + + public function testCreateUserTwoBackendExists(): void { + $this->expectException(\Exception::class); + + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend1 + */ + $backend1 = $this->createMock(\Test\Util\User\Dummy::class); + $backend1->expects($this->any()) + ->method('implementsActions') + ->willReturn(true); + + $backend1->expects($this->never()) + ->method('createUser'); + + $backend1->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(false); + + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend2 + */ + $backend2 = $this->createMock(\Test\Util\User\Dummy::class); + $backend2->expects($this->any()) + ->method('implementsActions') + ->willReturn(true); + + $backend2->expects($this->never()) + ->method('createUser'); + + $backend2->expects($this->once()) + ->method('userExists') + ->with($this->equalTo('foo')) + ->willReturn(true); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend1); + $manager->registerBackend($backend2); + + $manager->createUser('foo', 'bar'); + } + + public function testCountUsersNoBackend(): void { + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + + $result = $manager->countUsers(); + $this->assertTrue(is_array($result)); + $this->assertTrue(empty($result)); + } + + public function testCountUsersOneBackend(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('countUsers') + ->willReturn(7); + + $backend->expects($this->once()) + ->method('implementsActions') + ->with(BACKEND::COUNT_USERS) + ->willReturn(true); + + $backend->expects($this->once()) + ->method('getBackendName') + ->willReturn('Mock_Test_Util_User_Dummy'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $result = $manager->countUsers(); + $keys = array_keys($result); + $this->assertTrue(strpos($keys[0], 'Mock_Test_Util_User_Dummy') !== false); + + $users = array_shift($result); + $this->assertEquals(7, $users); + } + + public function testCountUsersTwoBackends(): void { + /** + * @var \Test\Util\User\Dummy | \PHPUnit\Framework\MockObject\MockObject $backend + */ + $backend1 = $this->createMock(\Test\Util\User\Dummy::class); + $backend1->expects($this->once()) + ->method('countUsers') + ->willReturn(7); + + $backend1->expects($this->once()) + ->method('implementsActions') + ->with(BACKEND::COUNT_USERS) + ->willReturn(true); + $backend1->expects($this->once()) + ->method('getBackendName') + ->willReturn('Mock_Test_Util_User_Dummy'); + + $backend2 = $this->createMock(\Test\Util\User\Dummy::class); + $backend2->expects($this->once()) + ->method('countUsers') + ->willReturn(16); + + $backend2->expects($this->once()) + ->method('implementsActions') + ->with(BACKEND::COUNT_USERS) + ->willReturn(true); + $backend2->expects($this->once()) + ->method('getBackendName') + ->willReturn('Mock_Test_Util_User_Dummy'); + + $manager = new \OC\User\Manager($this->config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend1); + $manager->registerBackend($backend2); + + $result = $manager->countUsers(); + //because the backends have the same class name, only one value expected + $this->assertEquals(1, count($result)); + $keys = array_keys($result); + $this->assertTrue(strpos($keys[0], 'Mock_Test_Util_User_Dummy') !== false); + + $users = array_shift($result); + //users from backends shall be summed up + $this->assertEquals(7 + 16, $users); + } + + public function testCountUsersOnlyDisabled(): void { + $manager = Server::get(IUserManager::class); + // count other users in the db before adding our own + $countBefore = $manager->countDisabledUsers(); + + //Add test users + $user1 = $manager->createUser('testdisabledcount1', 'testdisabledcount1'); + + $user2 = $manager->createUser('testdisabledcount2', 'testdisabledcount2'); + $user2->setEnabled(false); + + $user3 = $manager->createUser('testdisabledcount3', 'testdisabledcount3'); + + $user4 = $manager->createUser('testdisabledcount4', 'testdisabledcount4'); + $user4->setEnabled(false); + + $this->assertEquals($countBefore + 2, $manager->countDisabledUsers()); + + //cleanup + $user1->delete(); + $user2->delete(); + $user3->delete(); + $user4->delete(); + } + + public function testCountUsersOnlySeen(): void { + $manager = Server::get(IUserManager::class); + // count other users in the db before adding our own + $countBefore = $manager->countSeenUsers(); + + //Add test users + $user1 = $manager->createUser('testseencount1', 'testseencount1'); + $user1->updateLastLoginTimestamp(); + + $user2 = $manager->createUser('testseencount2', 'testseencount2'); + $user2->updateLastLoginTimestamp(); + + $user3 = $manager->createUser('testseencount3', 'testseencount3'); + + $user4 = $manager->createUser('testseencount4', 'testseencount4'); + $user4->updateLastLoginTimestamp(); + + $this->assertEquals($countBefore + 3, $manager->countSeenUsers()); + + //cleanup + $user1->delete(); + $user2->delete(); + $user3->delete(); + $user4->delete(); + } + + public function testCallForSeenUsers(): void { + $manager = Server::get(IUserManager::class); + // count other users in the db before adding our own + $count = 0; + $function = function (IUser $user) use (&$count): void { + $count++; + }; + $manager->callForAllUsers($function, '', true); + $countBefore = $count; + + //Add test users + $user1 = $manager->createUser('testseen1', 'testseen10'); + $user1->updateLastLoginTimestamp(); + + $user2 = $manager->createUser('testseen2', 'testseen20'); + $user2->updateLastLoginTimestamp(); + + $user3 = $manager->createUser('testseen3', 'testseen30'); + + $user4 = $manager->createUser('testseen4', 'testseen40'); + $user4->updateLastLoginTimestamp(); + + $count = 0; + $manager->callForAllUsers($function, '', true); + + $this->assertEquals($countBefore + 3, $count); + + //cleanup + $user1->delete(); + $user2->delete(); + $user3->delete(); + $user4->delete(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testRecentlyActive(): void { + $config = Server::get(IConfig::class); + $manager = Server::get(IUserManager::class); + + // Create some users + $now = (string)time(); + $user1 = $manager->createUser('test_active_1', 'test_active_1'); + $config->setUserValue('test_active_1', 'login', 'lastLogin', $now); + $user1->setDisplayName('test active 1'); + $user1->setSystemEMailAddress('roger@active.com'); + + $user2 = $manager->createUser('TEST_ACTIVE_2_FRED', 'TEST_ACTIVE_2'); + $config->setUserValue('TEST_ACTIVE_2_FRED', 'login', 'lastLogin', $now); + $user2->setDisplayName('TEST ACTIVE 2 UPPER'); + $user2->setSystemEMailAddress('Fred@Active.Com'); + + $user3 = $manager->createUser('test_active_3', 'test_active_3'); + $config->setUserValue('test_active_3', 'login', 'lastLogin', $now + 1); + $user3->setDisplayName('test active 3'); + + $user4 = $manager->createUser('test_active_4', 'test_active_4'); + $config->setUserValue('test_active_4', 'login', 'lastLogin', $now); + $user4->setDisplayName('Test Active 4'); + + $user5 = $manager->createUser('test_inactive_1', 'test_inactive_1'); + $user5->setDisplayName('Test Inactive 1'); + $user2->setSystemEMailAddress('jeanne@Active.Com'); + + // Search recently active + // - No search, case-insensitive order + $users = $manager->getLastLoggedInUsers(4); + $this->assertEquals(['test_active_3', 'test_active_1', 'TEST_ACTIVE_2_FRED', 'test_active_4'], $users); + // - Search, case-insensitive order + $users = $manager->getLastLoggedInUsers(search: 'act'); + $this->assertEquals(['test_active_3', 'test_active_1', 'TEST_ACTIVE_2_FRED', 'test_active_4'], $users); + // - No search with offset + $users = $manager->getLastLoggedInUsers(2, 2); + $this->assertEquals(['TEST_ACTIVE_2_FRED', 'test_active_4'], $users); + // - Case insensitive search (email) + $users = $manager->getLastLoggedInUsers(search: 'active.com'); + $this->assertEquals(['test_active_1', 'TEST_ACTIVE_2_FRED'], $users); + // - Case insensitive search (display name) + $users = $manager->getLastLoggedInUsers(search: 'upper'); + $this->assertEquals(['TEST_ACTIVE_2_FRED'], $users); + // - Case insensitive search (uid) + $users = $manager->getLastLoggedInUsers(search: 'fred'); + $this->assertEquals(['TEST_ACTIVE_2_FRED'], $users); + + // Delete users and config keys + $user1->delete(); + $user2->delete(); + $user3->delete(); + $user4->delete(); + $user5->delete(); + } + + public function testDeleteUser(): void { + $config = $this->getMockBuilder(AllConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $config + ->expects($this->any()) + ->method('getUserValue') + ->willReturnArgument(3); + $config + ->expects($this->any()) + ->method('getAppValue') + ->willReturnArgument(2); + + $manager = new \OC\User\Manager($config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $backend = new \Test\Util\User\Dummy(); + + $manager->registerBackend($backend); + $backend->createUser('foo', 'bar'); + $this->assertTrue($manager->userExists('foo')); + $manager->get('foo')->delete(); + $this->assertFalse($manager->userExists('foo')); + } + + public function testGetByEmail(): void { + $config = $this->getMockBuilder(AllConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $config + ->expects($this->once()) + ->method('getUsersForUserValueCaseInsensitive') + ->with('settings', 'email', 'test@example.com') + ->willReturn(['uid1', 'uid99', 'uid2']); + + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->exactly(3)) + ->method('userExists') + ->willReturnMap([ + ['uid1', true], + ['uid99', false], + ['uid2', true] + ]); + + $manager = new \OC\User\Manager($config, $this->cacheFactory, $this->eventDispatcher, $this->logger); + $manager->registerBackend($backend); + + $users = $manager->getByEmail('test@example.com'); + $this->assertCount(2, $users); + $this->assertEquals('uid1', $users[0]->getUID()); + $this->assertEquals('uid2', $users[1]->getUID()); + } +} diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php new file mode 100644 index 00000000000..50c449559a0 --- /dev/null +++ b/tests/lib/User/SessionTest.php @@ -0,0 +1,1324 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\User; + +use OC\AppFramework\Http\Request; +use OC\Authentication\Events\LoginFailed; +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\Exceptions\PasswordLoginForbiddenException; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; +use OC\Authentication\Token\PublicKeyToken; +use OC\Security\CSRF\CsrfTokenManager; +use OC\Session\Memory; +use OC\User\LoginException; +use OC\User\Manager; +use OC\User\Session; +use OC\User\User; +use OCA\DAV\Connector\Sabre\Auth; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IRequestId; +use OCP\ISession; +use OCP\IUser; +use OCP\Lockdown\ILockdownManager; +use OCP\Security\Bruteforce\IThrottler; +use OCP\Security\ISecureRandom; +use OCP\User\Events\PostLoginEvent; +use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use function array_diff; +use function get_class_methods; + +/** + * @group DB + * @package Test\User + */ +class SessionTest extends \Test\TestCase { + /** @var ITimeFactory|MockObject */ + private $timeFactory; + /** @var IProvider|MockObject */ + private $tokenProvider; + /** @var IConfig|MockObject */ + private $config; + /** @var IThrottler|MockObject */ + private $throttler; + /** @var ISecureRandom|MockObject */ + private $random; + /** @var Manager|MockObject */ + private $manager; + /** @var ISession|MockObject */ + private $session; + /** @var Session|MockObject */ + private $userSession; + /** @var ILockdownManager|MockObject */ + private $lockdownManager; + /** @var LoggerInterface|MockObject */ + private $logger; + /** @var IEventDispatcher|MockObject */ + private $dispatcher; + + protected function setUp(): void { + parent::setUp(); + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->willReturn(10000); + $this->tokenProvider = $this->createMock(IProvider::class); + $this->config = $this->createMock(IConfig::class); + $this->throttler = $this->createMock(IThrottler::class); + $this->random = $this->createMock(ISecureRandom::class); + $this->manager = $this->createMock(Manager::class); + $this->session = $this->createMock(ISession::class); + $this->lockdownManager = $this->createMock(ILockdownManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->dispatcher = $this->createMock(IEventDispatcher::class); + $this->userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([ + $this->manager, + $this->session, + $this->timeFactory, + $this->tokenProvider, + $this->config, + $this->random, + $this->lockdownManager, + $this->logger, + $this->dispatcher + ]) + ->onlyMethods([ + 'setMagicInCookie', + ]) + ->getMock(); + + \OC_User::setIncognitoMode(false); + } + + public static function isLoggedInData(): array { + return [ + [true], + [false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('isLoggedInData')] + public function testIsLoggedIn($isLoggedIn): void { + $session = $this->createMock(Memory::class); + + $manager = $this->createMock(Manager::class); + + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods([ + 'getUser' + ]) + ->getMock(); + $user = new User('sepp', null, $this->createMock(IEventDispatcher::class)); + $userSession->expects($this->once()) + ->method('getUser') + ->willReturn($isLoggedIn ? $user : null); + $this->assertEquals($isLoggedIn, $userSession->isLoggedIn()); + } + + public function testSetUser(): void { + $session = $this->createMock(Memory::class); + $session->expects($this->once()) + ->method('set') + ->with('user_id', 'foo'); + + $manager = $this->createMock(Manager::class); + + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $user = $this->createMock(IUser::class); + $user->expects($this->once()) + ->method('getUID') + ->willReturn('foo'); + + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + $userSession->setUser($user); + } + + public function testLoginValidPasswordEnabled(): void { + $session = $this->createMock(Memory::class); + $session->expects($this->once()) + ->method('regenerateId'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('bar') + ->willThrowException(new InvalidTokenException()); + $session->expects($this->exactly(2)) + ->method('set') + ->with($this->callback(function ($key) { + switch ($key) { + case 'user_id': + case 'loginname': + return true; + break; + default: + return false; + break; + } + }, 'foo')); + + $managerMethods = get_class_methods(Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->any()) + ->method('getUID') + ->willReturn('foo'); + $user->expects($this->once()) + ->method('updateLastLoginTimestamp'); + + $manager->expects($this->once()) + ->method('checkPasswordNoLogging') + ->with('foo', 'bar') + ->willReturn($user); + + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods([ + 'prepareUserLogin' + ]) + ->getMock(); + $userSession->expects($this->once()) + ->method('prepareUserLogin'); + + $this->dispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with( + $this->callback(function (PostLoginEvent $e) { + return $e->getUser()->getUID() === 'foo' + && $e->getPassword() === 'bar' + && $e->isTokenLogin() === false; + }) + ); + + $userSession->login('foo', 'bar'); + $this->assertEquals($user, $userSession->getUser()); + } + + + public function testLoginValidPasswordDisabled(): void { + $this->expectException(LoginException::class); + + $session = $this->createMock(Memory::class); + $session->expects($this->never()) + ->method('set'); + $session->expects($this->once()) + ->method('regenerateId'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('bar') + ->willThrowException(new InvalidTokenException()); + + $managerMethods = get_class_methods(Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + + $user = $this->createMock(IUser::class); + $user->expects($this->any()) + ->method('isEnabled') + ->willReturn(false); + $user->expects($this->never()) + ->method('updateLastLoginTimestamp'); + + $manager->expects($this->once()) + ->method('checkPasswordNoLogging') + ->with('foo', 'bar') + ->willReturn($user); + + $this->dispatcher->expects($this->never()) + ->method('dispatch'); + + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + $userSession->login('foo', 'bar'); + } + + public function testLoginInvalidPassword(): void { + $session = $this->createMock(Memory::class); + $managerMethods = get_class_methods(Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + + $user = $this->createMock(IUser::class); + + $session->expects($this->never()) + ->method('set'); + $session->expects($this->once()) + ->method('regenerateId'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('bar') + ->willThrowException(new InvalidTokenException()); + + $user->expects($this->never()) + ->method('isEnabled'); + $user->expects($this->never()) + ->method('updateLastLoginTimestamp'); + + $manager->expects($this->once()) + ->method('checkPasswordNoLogging') + ->with('foo', 'bar') + ->willReturn(false); + + $this->dispatcher->expects($this->never()) + ->method('dispatch'); + + $userSession->login('foo', 'bar'); + } + + public function testPasswordlessLoginNoLastCheckUpdate(): void { + $session = $this->createMock(Memory::class); + $managerMethods = get_class_methods(Manager::class); + // Keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + + $session->expects($this->never()) + ->method('set'); + $session->expects($this->once()) + ->method('regenerateId'); + $token = new PublicKeyToken(); + $token->setLoginName('foo'); + $token->setLastCheck(0); // Never + $token->setUid('foo'); + $this->tokenProvider + ->method('getPassword') + ->with($token) + ->willThrowException(new PasswordlessTokenException()); + $this->tokenProvider + ->method('getToken') + ->with('app-password') + ->willReturn($token); + $this->tokenProvider->expects(self::never()) + ->method('updateToken'); + + $userSession->login('foo', 'app-password'); + } + + public function testLoginLastCheckUpdate(): void { + $session = $this->createMock(Memory::class); + $managerMethods = get_class_methods(Manager::class); + // Keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + + $session->expects($this->never()) + ->method('set'); + $session->expects($this->once()) + ->method('regenerateId'); + $token = new PublicKeyToken(); + $token->setLoginName('foo'); + $token->setLastCheck(0); // Never + $token->setUid('foo'); + $this->tokenProvider + ->method('getPassword') + ->with($token) + ->willReturn('secret'); + $this->tokenProvider + ->method('getToken') + ->with('app-password') + ->willReturn($token); + $this->tokenProvider->expects(self::once()) + ->method('updateToken'); + + $userSession->login('foo', 'app-password'); + } + + public function testLoginNonExisting(): void { + $session = $this->createMock(Memory::class); + $manager = $this->createMock(Manager::class); + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + + $session->expects($this->never()) + ->method('set'); + $session->expects($this->once()) + ->method('regenerateId'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('bar') + ->willThrowException(new InvalidTokenException()); + + $manager->expects($this->once()) + ->method('checkPasswordNoLogging') + ->with('foo', 'bar') + ->willReturn(false); + + $userSession->login('foo', 'bar'); + } + + public function testLogClientInNoTokenPasswordWith2fa(): void { + $this->expectException(PasswordLoginForbiddenException::class); + + $manager = $this->createMock(Manager::class); + $session = $this->createMock(ISession::class); + $request = $this->createMock(IRequest::class); + + /** @var Session $userSession */ + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser']) + ->getMock(); + + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('doe') + ->willThrowException(new InvalidTokenException()); + $this->config->expects($this->once()) + ->method('getSystemValueBool') + ->with('token_auth_enforced', false) + ->willReturn(true); + $request + ->expects($this->any()) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->any()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + + $userSession->logClientIn('john', 'doe', $request, $this->throttler); + } + + public function testLogClientInUnexist(): void { + $manager = $this->createMock(Manager::class); + $session = $this->createMock(ISession::class); + $request = $this->createMock(IRequest::class); + + /** @var Session $userSession */ + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods(['login', 'supportsCookies', 'createSessionToken', 'getUser']) + ->getMock(); + + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('doe') + ->willThrowException(new InvalidTokenException()); + $this->config->expects($this->once()) + ->method('getSystemValueBool') + ->with('token_auth_enforced', false) + ->willReturn(false); + $manager->method('getByEmail') + ->with('unexist') + ->willReturn([]); + + $this->assertFalse($userSession->logClientIn('unexist', 'doe', $request, $this->throttler)); + } + + public function testLogClientInWithTokenPassword(): void { + $manager = $this->createMock(Manager::class); + $session = $this->createMock(ISession::class); + $request = $this->createMock(IRequest::class); + + /** @var Session $userSession */ + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser']) + ->getMock(); + + $userSession->expects($this->once()) + ->method('isTokenPassword') + ->willReturn(true); + $userSession->expects($this->once()) + ->method('login') + ->with('john', 'I-AM-AN-APP-PASSWORD') + ->willReturn(true); + + $session->expects($this->once()) + ->method('set') + ->with('app_password', 'I-AM-AN-APP-PASSWORD'); + $request + ->expects($this->any()) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->any()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + + $this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request, $this->throttler)); + } + + + public function testLogClientInNoTokenPasswordNo2fa(): void { + $this->expectException(PasswordLoginForbiddenException::class); + + $manager = $this->createMock(Manager::class); + $session = $this->createMock(ISession::class); + $request = $this->createMock(IRequest::class); + + /** @var Session $userSession */ + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods(['login', 'isTwoFactorEnforced']) + ->getMock(); + + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('doe') + ->willThrowException(new InvalidTokenException()); + $this->config->expects($this->once()) + ->method('getSystemValueBool') + ->with('token_auth_enforced', false) + ->willReturn(false); + + $userSession->expects($this->once()) + ->method('isTwoFactorEnforced') + ->with('john') + ->willReturn(true); + + $request + ->expects($this->any()) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelayOrThrowOnMax') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->any()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + + $userSession->logClientIn('john', 'doe', $request, $this->throttler); + } + + public function testTryTokenLoginNoHeaderNoSessionCookie(): void { + $request = $this->createMock(IRequest::class); + $this->config->expects(self::once()) + ->method('getSystemValueString') + ->with('instanceid') + ->willReturn('abc123'); + $request->method('getHeader')->with('Authorization')->willReturn(''); + $request->method('getCookie')->with('abc123')->willReturn(null); + $this->tokenProvider->expects(self::never()) + ->method('getToken'); + + $loginResult = $this->userSession->tryTokenLogin($request); + + self::assertFalse($loginResult); + } + + public function testTryTokenLoginAuthorizationHeaderTokenNotFound(): void { + $request = $this->createMock(IRequest::class); + $request->method('getHeader')->with('Authorization')->willReturn('Bearer abcde-12345'); + $this->tokenProvider->expects(self::once()) + ->method('getToken') + ->with('abcde-12345') + ->willThrowException(new InvalidTokenException()); + + $loginResult = $this->userSession->tryTokenLogin($request); + + self::assertFalse($loginResult); + } + + public function testTryTokenLoginSessionIdTokenNotFound(): void { + $request = $this->createMock(IRequest::class); + $this->config->expects(self::once()) + ->method('getSystemValueString') + ->with('instanceid') + ->willReturn('abc123'); + $request->method('getHeader')->with('Authorization')->willReturn(''); + $request->method('getCookie')->with('abc123')->willReturn('abcde12345'); + $this->session->expects(self::once()) + ->method('getId') + ->willReturn('abcde12345'); + $this->tokenProvider->expects(self::once()) + ->method('getToken') + ->with('abcde12345') + ->willThrowException(new InvalidTokenException()); + + $loginResult = $this->userSession->tryTokenLogin($request); + + self::assertFalse($loginResult); + } + + public function testTryTokenLoginNotAnAppPassword(): void { + $request = $this->createMock(IRequest::class); + $this->config->expects(self::once()) + ->method('getSystemValueString') + ->with('instanceid') + ->willReturn('abc123'); + $request->method('getHeader')->with('Authorization')->willReturn(''); + $request->method('getCookie')->with('abc123')->willReturn('abcde12345'); + $this->session->expects(self::once()) + ->method('getId') + ->willReturn('abcde12345'); + $dbToken = new PublicKeyToken(); + $dbToken->setId(42); + $dbToken->setUid('johnny'); + $dbToken->setLoginName('johnny'); + $dbToken->setLastCheck(0); + $dbToken->setType(IToken::TEMPORARY_TOKEN); + $dbToken->setRemember(IToken::REMEMBER); + $this->tokenProvider->expects(self::any()) + ->method('getToken') + ->with('abcde12345') + ->willReturn($dbToken); + $this->session->method('set') + ->willReturnCallback(function ($key, $value): void { + if ($key === 'app_password') { + throw new ExpectationFailedException('app_password should not be set in session'); + } + }); + $user = $this->createMock(IUser::class); + $user->method('isEnabled')->willReturn(true); + $this->manager->method('get') + ->with('johnny') + ->willReturn($user); + + $loginResult = $this->userSession->tryTokenLogin($request); + + self::assertTrue($loginResult); + } + + public function testRememberLoginValidToken(): void { + $session = $this->createMock(Memory::class); + $managerMethods = get_class_methods(Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->onlyMethods(['setMagicInCookie', 'setLoginName']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->getMock(); + + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + $sessionId = 'sess123'; + + $session->expects($this->once()) + ->method('regenerateId'); + $manager->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($user); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->willReturn([$token]); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); + $this->random->expects($this->once()) + ->method('generate') + ->with(32) + ->willReturn('abcdefg123456'); + $this->config->expects($this->once()) + ->method('setUserValue') + ->with('foo', 'login_token', 'abcdefg123456', 10000); + + $tokenObject = $this->createMock(IToken::class); + $tokenObject->expects($this->once()) + ->method('getLoginName') + ->willReturn('foobar'); + $tokenObject->method('getId') + ->willReturn(42); + + $session->expects($this->once()) + ->method('getId') + ->willReturn($sessionId); + $this->tokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with($oldSessionId, $sessionId) + ->willReturn($tokenObject); + + $this->tokenProvider->expects($this->never()) + ->method('getToken'); + + $user->expects($this->any()) + ->method('getUID') + ->willReturn('foo'); + $userSession->expects($this->once()) + ->method('setMagicInCookie'); + $user->expects($this->once()) + ->method('updateLastLoginTimestamp'); + $setUID = false; + $session + ->method('set') + ->willReturnCallback(function ($k, $v) use (&$setUID): void { + if ($k === 'user_id' && $v === 'foo') { + $setUID = true; + } + }); + $userSession->expects($this->once()) + ->method('setLoginName') + ->willReturn('foobar'); + + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); + + $this->assertTrue($setUID); + + $this->assertTrue($granted); + } + + public function testRememberLoginInvalidSessionToken(): void { + $session = $this->createMock(Memory::class); + $managerMethods = get_class_methods(Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->onlyMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->getMock(); + + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + $sessionId = 'sess123'; + + $session->expects($this->once()) + ->method('regenerateId'); + $manager->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($user); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->willReturn([$token]); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); + $this->config->expects($this->once()) + ->method('setUserValue'); // TODO: mock new random value + + $session->expects($this->once()) + ->method('getId') + ->willReturn($sessionId); + $this->tokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with($oldSessionId, $sessionId) + ->willThrowException(new InvalidTokenException()); + + $user->expects($this->never()) + ->method('getUID') + ->willReturn('foo'); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); + $user->expects($this->never()) + ->method('updateLastLoginTimestamp'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); + + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); + + $this->assertFalse($granted); + } + + public function testRememberLoginInvalidToken(): void { + $session = $this->createMock(Memory::class); + $managerMethods = get_class_methods(Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->onlyMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->getMock(); + + $user = $this->createMock(IUser::class); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + + $session->expects($this->once()) + ->method('regenerateId'); + $manager->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn($user); + $this->config->expects($this->once()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->willReturn(['anothertoken']); + $this->config->expects($this->never()) + ->method('deleteUserValue') + ->with('foo', 'login_token', $token); + + $this->tokenProvider->expects($this->never()) + ->method('renewSessionToken'); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); + $user->expects($this->never()) + ->method('updateLastLoginTimestamp'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); + + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); + + $this->assertFalse($granted); + } + + public function testRememberLoginInvalidUser(): void { + $session = $this->createMock(Memory::class); + $managerMethods = get_class_methods(Manager::class); + //keep following methods intact in order to ensure hooks are working + $mockedManagerMethods = array_diff($managerMethods, ['__construct', 'emit', 'listen']); + $manager = $this->getMockBuilder(Manager::class) + ->onlyMethods($mockedManagerMethods) + ->setConstructorArgs([ + $this->config, + $this->createMock(ICacheFactory::class), + $this->createMock(IEventDispatcher::class), + $this->createMock(LoggerInterface::class), + ]) + ->getMock(); + $userSession = $this->getMockBuilder(Session::class) + //override, otherwise tests will fail because of setcookie() + ->onlyMethods(['setMagicInCookie']) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->getMock(); + $token = 'goodToken'; + $oldSessionId = 'sess321'; + + $session->expects($this->once()) + ->method('regenerateId'); + $manager->expects($this->once()) + ->method('get') + ->with('foo') + ->willReturn(null); + $this->config->expects($this->never()) + ->method('getUserKeys') + ->with('foo', 'login_token') + ->willReturn(['anothertoken']); + + $this->tokenProvider->expects($this->never()) + ->method('renewSessionToken'); + $userSession->expects($this->never()) + ->method('setMagicInCookie'); + $session->expects($this->never()) + ->method('set') + ->with('user_id', 'foo'); + + $granted = $userSession->loginWithCookie('foo', $token, $oldSessionId); + + $this->assertFalse($granted); + } + + public function testActiveUserAfterSetSession(): void { + $users = [ + 'foo' => new User('foo', null, $this->createMock(IEventDispatcher::class)), + 'bar' => new User('bar', null, $this->createMock(IEventDispatcher::class)) + ]; + + $manager = $this->getMockBuilder(Manager::class) + ->disableOriginalConstructor() + ->getMock(); + + $manager->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($uid) use ($users) { + return $users[$uid]; + }); + + $session = new Memory(); + $session->set('user_id', 'foo'); + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods([ + 'validateSession' + ]) + ->getMock(); + $userSession->expects($this->any()) + ->method('validateSession'); + + $this->assertEquals($users['foo'], $userSession->getUser()); + + $session2 = new Memory(); + $session2->set('user_id', 'bar'); + $userSession->setSession($session2); + $this->assertEquals($users['bar'], $userSession->getUser()); + } + + public function testCreateSessionToken(): void { + $manager = $this->createMock(Manager::class); + $session = $this->createMock(ISession::class); + $user = $this->createMock(IUser::class); + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + + $requestId = $this->createMock(IRequestId::class); + $config = $this->createMock(IConfig::class); + $csrf = $this->getMockBuilder(CsrfTokenManager::class) + ->disableOriginalConstructor() + ->getMock(); + $request = new Request([ + 'server' => [ + 'HTTP_USER_AGENT' => 'Firefox', + ] + ], $requestId, $config, $csrf); + + $uid = 'user123'; + $loginName = 'User123'; + $password = 'passme'; + $sessionId = 'abcxyz'; + + $manager->expects($this->once()) + ->method('get') + ->with($uid) + ->willReturn($user); + $session->expects($this->once()) + ->method('getId') + ->willReturn($sessionId); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with($password) + ->willThrowException(new InvalidTokenException()); + + $this->tokenProvider->expects($this->once()) + ->method('generateToken') + ->with($sessionId, $uid, $loginName, $password, 'Firefox', IToken::TEMPORARY_TOKEN, IToken::DO_NOT_REMEMBER); + + $this->assertTrue($userSession->createSessionToken($request, $uid, $loginName, $password)); + } + + public function testCreateRememberedSessionToken(): void { + $manager = $this->createMock(Manager::class); + $session = $this->createMock(ISession::class); + $user = $this->createMock(IUser::class); + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + + $requestId = $this->createMock(IRequestId::class); + $config = $this->createMock(IConfig::class); + $csrf = $this->getMockBuilder(CsrfTokenManager::class) + ->disableOriginalConstructor() + ->getMock(); + $request = new Request([ + 'server' => [ + 'HTTP_USER_AGENT' => 'Firefox', + ] + ], $requestId, $config, $csrf); + + $uid = 'user123'; + $loginName = 'User123'; + $password = 'passme'; + $sessionId = 'abcxyz'; + + $manager->expects($this->once()) + ->method('get') + ->with($uid) + ->willReturn($user); + $session->expects($this->once()) + ->method('getId') + ->willReturn($sessionId); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with($password) + ->willThrowException(new InvalidTokenException()); + + $this->tokenProvider->expects($this->once()) + ->method('generateToken') + ->with($sessionId, $uid, $loginName, $password, 'Firefox', IToken::TEMPORARY_TOKEN, IToken::REMEMBER); + + $this->assertTrue($userSession->createSessionToken($request, $uid, $loginName, $password, true)); + } + + public function testCreateSessionTokenWithTokenPassword(): void { + $manager = $this->getMockBuilder(Manager::class) + ->disableOriginalConstructor() + ->getMock(); + $session = $this->createMock(ISession::class); + $token = $this->createMock(IToken::class); + $user = $this->createMock(IUser::class); + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + + $requestId = $this->createMock(IRequestId::class); + $config = $this->createMock(IConfig::class); + $csrf = $this->getMockBuilder(CsrfTokenManager::class) + ->disableOriginalConstructor() + ->getMock(); + $request = new Request([ + 'server' => [ + 'HTTP_USER_AGENT' => 'Firefox', + ] + ], $requestId, $config, $csrf); + + $uid = 'user123'; + $loginName = 'User123'; + $password = 'iamatoken'; + $realPassword = 'passme'; + $sessionId = 'abcxyz'; + + $manager->expects($this->once()) + ->method('get') + ->with($uid) + ->willReturn($user); + $session->expects($this->once()) + ->method('getId') + ->willReturn($sessionId); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with($password) + ->willReturn($token); + $this->tokenProvider->expects($this->once()) + ->method('getPassword') + ->with($token, $password) + ->willReturn($realPassword); + + $this->tokenProvider->expects($this->once()) + ->method('generateToken') + ->with($sessionId, $uid, $loginName, $realPassword, 'Firefox', IToken::TEMPORARY_TOKEN, IToken::DO_NOT_REMEMBER); + + $this->assertTrue($userSession->createSessionToken($request, $uid, $loginName, $password)); + } + + public function testCreateSessionTokenWithNonExistentUser(): void { + $manager = $this->getMockBuilder(Manager::class) + ->disableOriginalConstructor() + ->getMock(); + $session = $this->createMock(ISession::class); + $userSession = new Session($manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher); + $request = $this->createMock(IRequest::class); + + $uid = 'user123'; + $loginName = 'User123'; + $password = 'passme'; + + $manager->expects($this->once()) + ->method('get') + ->with($uid) + ->willReturn(null); + + $this->assertFalse($userSession->createSessionToken($request, $uid, $loginName, $password)); + } + + public function testCreateRememberMeToken(): void { + $user = $this->createMock(IUser::class); + $user + ->expects($this->exactly(2)) + ->method('getUID') + ->willReturn('UserUid'); + $this->random + ->expects($this->once()) + ->method('generate') + ->with(32) + ->willReturn('LongRandomToken'); + $this->config + ->expects($this->once()) + ->method('setUserValue') + ->with('UserUid', 'login_token', 'LongRandomToken', 10000); + $this->userSession + ->expects($this->once()) + ->method('setMagicInCookie') + ->with('UserUid', 'LongRandomToken'); + + $this->userSession->createRememberMeToken($user); + } + + public function testTryBasicAuthLoginValid(): void { + $request = $this->createMock(Request::class); + $request->method('__get') + ->willReturn([ + 'PHP_AUTH_USER' => 'username', + 'PHP_AUTH_PW' => 'password', + ]); + $request->method('__isset') + ->with('server') + ->willReturn(true); + + $davAuthenticatedSet = false; + $lastPasswordConfirmSet = false; + + $this->session + ->method('set') + ->willReturnCallback(function ($k, $v) use (&$davAuthenticatedSet, &$lastPasswordConfirmSet): void { + switch ($k) { + case Auth::DAV_AUTHENTICATED: + $davAuthenticatedSet = $v; + return; + case 'last-password-confirm': + $lastPasswordConfirmSet = 1000; + return; + default: + throw new \Exception(); + } + }); + + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([ + $this->manager, + $this->session, + $this->timeFactory, + $this->tokenProvider, + $this->config, + $this->random, + $this->lockdownManager, + $this->logger, + $this->dispatcher + ]) + ->onlyMethods([ + 'logClientIn', + 'getUser', + ]) + ->getMock(); + + /** @var Session|MockObject */ + $userSession->expects($this->once()) + ->method('logClientIn') + ->with( + $this->equalTo('username'), + $this->equalTo('password'), + $this->equalTo($request), + $this->equalTo($this->throttler) + )->willReturn(true); + + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('username'); + + $userSession->expects($this->once()) + ->method('getUser') + ->willReturn($user); + + $this->assertTrue($userSession->tryBasicAuthLogin($request, $this->throttler)); + + $this->assertSame('username', $davAuthenticatedSet); + $this->assertSame(1000, $lastPasswordConfirmSet); + } + + public function testTryBasicAuthLoginNoLogin(): void { + $request = $this->createMock(Request::class); + $request->method('__get') + ->willReturn([]); + $request->method('__isset') + ->with('server') + ->willReturn(true); + + $this->session->expects($this->never()) + ->method($this->anything()); + + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([ + $this->manager, + $this->session, + $this->timeFactory, + $this->tokenProvider, + $this->config, + $this->random, + $this->lockdownManager, + $this->logger, + $this->dispatcher + ]) + ->onlyMethods([ + 'logClientIn', + ]) + ->getMock(); + + /** @var Session|MockObject */ + $userSession->expects($this->never()) + ->method('logClientIn'); + + $this->assertFalse($userSession->tryBasicAuthLogin($request, $this->throttler)); + } + + public function testUpdateTokens(): void { + $this->tokenProvider->expects($this->once()) + ->method('updatePasswords') + ->with('uid', 'pass'); + + $this->userSession->updateTokens('uid', 'pass'); + } + + public function testLogClientInThrottlerUsername(): void { + $manager = $this->createMock(Manager::class); + $session = $this->createMock(ISession::class); + $request = $this->createMock(IRequest::class); + + /** @var Session $userSession */ + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser']) + ->getMock(); + + $userSession->expects($this->once()) + ->method('isTokenPassword') + ->willReturn(true); + $userSession->expects($this->once()) + ->method('login') + ->with('john', 'I-AM-AN-PASSWORD') + ->willReturn(false); + + $session->expects($this->never()) + ->method('set'); + $request + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->exactly(2)) + ->method('sleepDelayOrThrowOnMax') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->any()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('login', '192.168.0.1', ['user' => 'john']); + $this->dispatcher + ->expects($this->once()) + ->method('dispatchTyped') + ->with(new LoginFailed('john', 'I-AM-AN-PASSWORD')); + + $this->assertFalse($userSession->logClientIn('john', 'I-AM-AN-PASSWORD', $request, $this->throttler)); + } + + public function testLogClientInThrottlerEmail(): void { + $manager = $this->createMock(Manager::class); + $session = $this->createMock(ISession::class); + $request = $this->createMock(IRequest::class); + + /** @var Session $userSession */ + $userSession = $this->getMockBuilder(Session::class) + ->setConstructorArgs([$manager, $session, $this->timeFactory, $this->tokenProvider, $this->config, $this->random, $this->lockdownManager, $this->logger, $this->dispatcher]) + ->onlyMethods(['isTokenPassword', 'login', 'supportsCookies', 'createSessionToken', 'getUser']) + ->getMock(); + + $userSession->expects($this->once()) + ->method('isTokenPassword') + ->willReturn(false); + $userSession->expects($this->once()) + ->method('login') + ->with('john@foo.bar', 'I-AM-AN-PASSWORD') + ->willReturn(false); + $manager + ->method('getByEmail') + ->with('john@foo.bar') + ->willReturn([]); + + $session->expects($this->never()) + ->method('set'); + $request + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->exactly(2)) + ->method('sleepDelayOrThrowOnMax') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->any()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('login', '192.168.0.1', ['user' => 'john@foo.bar']); + $this->dispatcher + ->expects($this->once()) + ->method('dispatchTyped') + ->with(new LoginFailed('john@foo.bar', 'I-AM-AN-PASSWORD')); + + $this->assertFalse($userSession->logClientIn('john@foo.bar', 'I-AM-AN-PASSWORD', $request, $this->throttler)); + } +} diff --git a/tests/lib/User/UserTest.php b/tests/lib/User/UserTest.php new file mode 100644 index 00000000000..05056c92193 --- /dev/null +++ b/tests/lib/User/UserTest.php @@ -0,0 +1,1032 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\User; + +use OC\AllConfig; +use OC\Files\Mount\ObjectHomeMountProvider; +use OC\Hooks\PublicEmitter; +use OC\User\Database; +use OC\User\User; +use OCP\Comments\ICommentsManager; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\FileInfo; +use OCP\Files\Storage\IStorageFactory; +use OCP\IConfig; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; +use OCP\Server; +use OCP\UserInterface; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +/** + * Class UserTest + * + * @group DB + * + * @package Test\User + */ +class UserTest extends TestCase { + /** @var IEventDispatcher|MockObject */ + protected $dispatcher; + + protected function setUp(): void { + parent::setUp(); + $this->dispatcher = Server::get(IEventDispatcher::class); + } + + public function testDisplayName(): void { + /** + * @var \OC\User\Backend | MockObject $backend + */ + $backend = $this->createMock(\OC\User\Backend::class); + $backend->expects($this->once()) + ->method('getDisplayName') + ->with($this->equalTo('foo')) + ->willReturn('Foo'); + + $backend->expects($this->any()) + ->method('implementsActions') + ->with($this->equalTo(\OC\User\Backend::GET_DISPLAYNAME)) + ->willReturn(true); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertEquals('Foo', $user->getDisplayName()); + } + + /** + * if the display name contain whitespaces only, we expect the uid as result + */ + public function testDisplayNameEmpty(): void { + /** + * @var \OC\User\Backend | MockObject $backend + */ + $backend = $this->createMock(\OC\User\Backend::class); + $backend->expects($this->once()) + ->method('getDisplayName') + ->with($this->equalTo('foo')) + ->willReturn(' '); + + $backend->expects($this->any()) + ->method('implementsActions') + ->with($this->equalTo(\OC\User\Backend::GET_DISPLAYNAME)) + ->willReturn(true); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertEquals('foo', $user->getDisplayName()); + } + + public function testDisplayNameNotSupported(): void { + /** + * @var \OC\User\Backend | MockObject $backend + */ + $backend = $this->createMock(\OC\User\Backend::class); + $backend->expects($this->never()) + ->method('getDisplayName'); + + $backend->expects($this->any()) + ->method('implementsActions') + ->with($this->equalTo(\OC\User\Backend::GET_DISPLAYNAME)) + ->willReturn(false); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertEquals('foo', $user->getDisplayName()); + } + + public function testSetPassword(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('setPassword') + ->with($this->equalTo('foo'), $this->equalTo('bar')); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::SET_PASSWORD) { + return true; + } else { + return false; + } + }); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertTrue($user->setPassword('bar', '')); + } + + public function testSetPasswordNotSupported(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->never()) + ->method('setPassword'); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(false); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertFalse($user->setPassword('bar', '')); + } + + public function testChangeAvatarSupportedYes(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(AvatarUserDummy::class); + $backend->expects($this->once()) + ->method('canChangeAvatar') + ->with($this->equalTo('foo')) + ->willReturn(true); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::PROVIDE_AVATAR) { + return true; + } else { + return false; + } + }); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertTrue($user->canChangeAvatar()); + } + + public function testChangeAvatarSupportedNo(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(AvatarUserDummy::class); + $backend->expects($this->once()) + ->method('canChangeAvatar') + ->with($this->equalTo('foo')) + ->willReturn(false); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::PROVIDE_AVATAR) { + return true; + } else { + return false; + } + }); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertFalse($user->canChangeAvatar()); + } + + public function testChangeAvatarNotSupported(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(AvatarUserDummy::class); + $backend->expects($this->never()) + ->method('canChangeAvatar'); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(false); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertTrue($user->canChangeAvatar()); + } + + public function testDelete(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('deleteUser') + ->with($this->equalTo('foo')); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertTrue($user->delete()); + } + + public function testDeleteWithDifferentHome(): void { + /** @var ObjectHomeMountProvider $homeProvider */ + $homeProvider = Server::get(ObjectHomeMountProvider::class); + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('foo'); + if ($homeProvider->getHomeMountForUser($user, $this->createMock(IStorageFactory::class)) !== null) { + $this->markTestSkipped('Skipping test for non local home storage'); + } + + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $backend->expects($this->once()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::GET_HOME) { + return true; + } else { + return false; + } + }); + + // important: getHome MUST be called before deleteUser because + // once the user is deleted, getHome implementations might not + // return anything + $backend->expects($this->once()) + ->method('getHome') + ->with($this->equalTo('foo')) + ->willReturn('/home/foo'); + + $backend->expects($this->once()) + ->method('deleteUser') + ->with($this->equalTo('foo')); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertTrue($user->delete()); + } + + public function testGetHome(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('getHome') + ->with($this->equalTo('foo')) + ->willReturn('/home/foo'); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::GET_HOME) { + return true; + } else { + return false; + } + }); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertEquals('/home/foo', $user->getHome()); + } + + public function testGetBackendClassName(): void { + $user = new User('foo', new \Test\Util\User\Dummy(), $this->dispatcher); + $this->assertEquals('Dummy', $user->getBackendClassName()); + $user = new User('foo', new Database(), $this->dispatcher); + $this->assertEquals('Database', $user->getBackendClassName()); + } + + public function testGetHomeNotSupported(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->never()) + ->method('getHome'); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(false); + + $allConfig = $this->getMockBuilder(IConfig::class) + ->disableOriginalConstructor() + ->getMock(); + $allConfig->expects($this->any()) + ->method('getUserValue') + ->willReturn(true); + $allConfig->expects($this->any()) + ->method('getSystemValueString') + ->with($this->equalTo('datadirectory')) + ->willReturn('arbitrary/path'); + + $user = new User('foo', $backend, $this->dispatcher, null, $allConfig); + $this->assertEquals('arbitrary/path/foo', $user->getHome()); + } + + public function testCanChangePassword(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::SET_PASSWORD) { + return true; + } else { + return false; + } + }); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertTrue($user->canChangePassword()); + } + + public function testCanChangePasswordNotSupported(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(false); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertFalse($user->canChangePassword()); + } + + public function testCanChangeDisplayName(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::SET_DISPLAYNAME) { + return true; + } else { + return false; + } + }); + + $config = $this->createMock(IConfig::class); + $config->method('getSystemValueBool') + ->with('allow_user_to_change_display_name') + ->willReturn(true); + + $user = new User('foo', $backend, $this->dispatcher, null, $config); + $this->assertTrue($user->canChangeDisplayName()); + } + + public function testCanChangeDisplayNameNotSupported(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(false); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertFalse($user->canChangeDisplayName()); + } + + public function testSetDisplayNameSupported(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(Database::class); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::SET_DISPLAYNAME) { + return true; + } else { + return false; + } + }); + + $backend->expects($this->once()) + ->method('setDisplayName') + ->with('foo', 'Foo') + ->willReturn(true); + + $user = new User('foo', $backend, $this->createMock(IEventDispatcher::class)); + $this->assertTrue($user->setDisplayName('Foo')); + $this->assertEquals('Foo', $user->getDisplayName()); + } + + /** + * don't allow display names containing whitespaces only + */ + public function testSetDisplayNameEmpty(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(Database::class); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::SET_DISPLAYNAME) { + return true; + } else { + return false; + } + }); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertFalse($user->setDisplayName(' ')); + $this->assertEquals('foo', $user->getDisplayName()); + } + + public function testSetDisplayNameNotSupported(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(Database::class); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturn(false); + + $backend->expects($this->never()) + ->method('setDisplayName'); + + $user = new User('foo', $backend, $this->dispatcher); + $this->assertFalse($user->setDisplayName('Foo')); + $this->assertEquals('foo', $user->getDisplayName()); + } + + public function testSetPasswordHooks(): void { + $hooksCalled = 0; + $test = $this; + + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('setPassword'); + + /** + * @param User $user + * @param string $password + */ + $hook = function ($user, $password) use ($test, &$hooksCalled): void { + $hooksCalled++; + $test->assertEquals('foo', $user->getUID()); + $test->assertEquals('bar', $password); + }; + + $emitter = new PublicEmitter(); + $emitter->listen('\OC\User', 'preSetPassword', $hook); + $emitter->listen('\OC\User', 'postSetPassword', $hook); + + $backend->expects($this->any()) + ->method('implementsActions') + ->willReturnCallback(function ($actions) { + if ($actions === \OC\User\Backend::SET_PASSWORD) { + return true; + } else { + return false; + } + }); + + $user = new User('foo', $backend, $this->dispatcher, $emitter); + + $user->setPassword('bar', ''); + $this->assertEquals(2, $hooksCalled); + } + + public static function dataDeleteHooks(): array { + return [ + [true, 2], + [false, 1], + ]; + } + + /** + * @param bool $result + * @param int $expectedHooks + */ + #[\PHPUnit\Framework\Attributes\DataProvider('dataDeleteHooks')] + public function testDeleteHooks($result, $expectedHooks): void { + $hooksCalled = 0; + $test = $this; + + /** + * @var UserInterface&MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('deleteUser') + ->willReturn($result); + + $config = $this->createMock(IConfig::class); + $config->method('getSystemValue') + ->willReturnArgument(1); + $config->method('getSystemValueString') + ->willReturnArgument(1); + $config->method('getSystemValueBool') + ->willReturnArgument(1); + $config->method('getSystemValueInt') + ->willReturnArgument(1); + + $emitter = new PublicEmitter(); + $user = new User('foo', $backend, $this->dispatcher, $emitter, $config); + + /** + * @param User $user + */ + $hook = function ($user) use ($test, &$hooksCalled): void { + $hooksCalled++; + $test->assertEquals('foo', $user->getUID()); + }; + + $emitter->listen('\OC\User', 'preDelete', $hook); + $emitter->listen('\OC\User', 'postDelete', $hook); + + $commentsManager = $this->createMock(ICommentsManager::class); + $notificationManager = $this->createMock(INotificationManager::class); + + if ($result) { + $config->expects($this->atLeastOnce()) + ->method('deleteAllUserValues') + ->with('foo'); + + $commentsManager->expects($this->once()) + ->method('deleteReferencesOfActor') + ->with('users', 'foo'); + $commentsManager->expects($this->once()) + ->method('deleteReadMarksFromUser') + ->with($user); + + $notification = $this->createMock(INotification::class); + $notification->expects($this->once()) + ->method('setUser') + ->with('foo'); + + $notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + $notificationManager->expects($this->once()) + ->method('markProcessed') + ->with($notification); + } else { + $config->expects($this->never()) + ->method('deleteAllUserValues'); + + $commentsManager->expects($this->never()) + ->method('deleteReferencesOfActor'); + $commentsManager->expects($this->never()) + ->method('deleteReadMarksFromUser'); + + $notificationManager->expects($this->never()) + ->method('createNotification'); + $notificationManager->expects($this->never()) + ->method('markProcessed'); + } + + $this->overwriteService(INotificationManager::class, $notificationManager); + $this->overwriteService(ICommentsManager::class, $commentsManager); + + $this->assertSame($result, $user->delete()); + + $this->restoreService(AllConfig::class); + $this->restoreService(ICommentsManager::class); + $this->restoreService(INotificationManager::class); + + $this->assertEquals($expectedHooks, $hooksCalled); + } + + public function testDeleteRecoverState() { + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $backend->expects($this->once()) + ->method('deleteUser') + ->willReturn(true); + + $config = $this->createMock(IConfig::class); + $config->method('getSystemValue') + ->willReturnArgument(1); + $config->method('getSystemValueString') + ->willReturnArgument(1); + $config->method('getSystemValueBool') + ->willReturnArgument(1); + $config->method('getSystemValueInt') + ->willReturnArgument(1); + + $userConfig = []; + $config->expects(self::atLeast(2)) + ->method('setUserValue') + ->willReturnCallback(function (): void { + $userConfig[] = func_get_args(); + }); + + $commentsManager = $this->createMock(ICommentsManager::class); + $commentsManager->expects($this->once()) + ->method('deleteReferencesOfActor') + ->willThrowException(new \Error('Test exception')); + + $this->overwriteService(ICommentsManager::class, $commentsManager); + $this->expectException(\Error::class); + + $user = $this->getMockBuilder(User::class) + ->onlyMethods(['getHome']) + ->setConstructorArgs(['foo', $backend, $this->dispatcher, null, $config]) + ->getMock(); + + $user->expects(self::atLeastOnce()) + ->method('getHome') + ->willReturn('/home/path'); + + $user->delete(); + + $this->assertEqualsCanonicalizing( + [ + ['foo', 'core', 'deleted', 'true', null], + ['foo', 'core', 'deleted.backup-home', '/home/path', null], + ], + $userConfig, + ); + + $this->restoreService(ICommentsManager::class); + } + + public static function dataGetCloudId(): array { + return [ + ['https://localhost:8888/nextcloud', 'foo@localhost:8888/nextcloud'], + ['http://localhost:8888/nextcloud', 'foo@http://localhost:8888/nextcloud'], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('dataGetCloudId')] + public function testGetCloudId(string $absoluteUrl, string $cloudId): void { + /** @var Backend|MockObject $backend */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + $urlGenerator = $this->createMock(IURLGenerator::class); + $urlGenerator->method('getAbsoluteURL') + ->withAnyParameters() + ->willReturn($absoluteUrl); + $user = new User('foo', $backend, $this->dispatcher, null, null, $urlGenerator); + $this->assertEquals($cloudId, $user->getCloudId()); + } + + public function testSetEMailAddressEmpty(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $test = $this; + $hooksCalled = 0; + + /** + * @param IUser $user + * @param string $feature + * @param string $value + */ + $hook = function (IUser $user, $feature, $value) use ($test, &$hooksCalled): void { + $hooksCalled++; + $test->assertEquals('eMailAddress', $feature); + $test->assertEquals('', $value); + }; + + $emitter = new PublicEmitter(); + $emitter->listen('\OC\User', 'changeUser', $hook); + + $config = $this->createMock(IConfig::class); + $config->expects($this->once()) + ->method('deleteUserValue') + ->with( + 'foo', + 'settings', + 'email' + ); + + $user = new User('foo', $backend, $this->dispatcher, $emitter, $config); + $user->setEMailAddress(''); + } + + public function testSetEMailAddress(): void { + /** + * @var UserInterface | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $test = $this; + $hooksCalled = 0; + + /** + * @param IUser $user + * @param string $feature + * @param string $value + */ + $hook = function (IUser $user, $feature, $value) use ($test, &$hooksCalled): void { + $hooksCalled++; + $test->assertEquals('eMailAddress', $feature); + $test->assertEquals('foo@bar.com', $value); + }; + + $emitter = new PublicEmitter(); + $emitter->listen('\OC\User', 'changeUser', $hook); + + $config = $this->createMock(IConfig::class); + $config->expects($this->once()) + ->method('setUserValue') + ->with( + 'foo', + 'settings', + 'email', + 'foo@bar.com' + ); + + $user = new User('foo', $backend, $this->dispatcher, $emitter, $config); + $user->setEMailAddress('foo@bar.com'); + } + + public function testSetEMailAddressNoChange(): void { + /** + * @var UserInterface | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + /** @var PublicEmitter|MockObject $emitter */ + $emitter = $this->createMock(PublicEmitter::class); + $emitter->expects($this->never()) + ->method('emit'); + + $dispatcher = $this->createMock(IEventDispatcher::class); + $dispatcher->expects($this->never()) + ->method('dispatch'); + + $config = $this->createMock(IConfig::class); + $config->expects($this->any()) + ->method('getUserValue') + ->willReturn('foo@bar.com'); + $config->expects($this->any()) + ->method('setUserValue'); + + $user = new User('foo', $backend, $dispatcher, $emitter, $config); + $user->setEMailAddress('foo@bar.com'); + } + + public function testSetQuota(): void { + /** + * @var UserInterface | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $test = $this; + $hooksCalled = 0; + + /** + * @param IUser $user + * @param string $feature + * @param string $value + */ + $hook = function (IUser $user, $feature, $value) use ($test, &$hooksCalled): void { + $hooksCalled++; + $test->assertEquals('quota', $feature); + $test->assertEquals('23 TB', $value); + }; + + $emitter = new PublicEmitter(); + $emitter->listen('\OC\User', 'changeUser', $hook); + + $config = $this->createMock(IConfig::class); + $config->expects($this->once()) + ->method('setUserValue') + ->with( + 'foo', + 'files', + 'quota', + '23 TB' + ); + + $user = new User('foo', $backend, $this->dispatcher, $emitter, $config); + $user->setQuota('23 TB'); + } + + public function testGetDefaultUnlimitedQuota(): void { + /** + * @var UserInterface | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + /** @var PublicEmitter|MockObject $emitter */ + $emitter = $this->createMock(PublicEmitter::class); + $emitter->expects($this->never()) + ->method('emit'); + + $config = $this->createMock(IConfig::class); + $user = new User('foo', $backend, $this->dispatcher, $emitter, $config); + + $userValueMap = [ + ['foo', 'files', 'quota', 'default', 'default'], + ]; + $appValueMap = [ + ['files', 'default_quota', 'none', 'none'], + // allow unlimited quota + ['files', 'allow_unlimited_quota', '1', '1'], + ]; + $config->method('getUserValue') + ->willReturnMap($userValueMap); + $config->method('getAppValue') + ->willReturnMap($appValueMap); + + $this->assertEquals('none', $user->getQuota()); + $this->assertEquals(FileInfo::SPACE_UNLIMITED, $user->getQuotaBytes()); + } + + public function testGetDefaultUnlimitedQuotaForbidden(): void { + /** + * @var UserInterface | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + /** @var PublicEmitter|MockObject $emitter */ + $emitter = $this->createMock(PublicEmitter::class); + $emitter->expects($this->never()) + ->method('emit'); + + $config = $this->createMock(IConfig::class); + $user = new User('foo', $backend, $this->dispatcher, $emitter, $config); + + $userValueMap = [ + ['foo', 'files', 'quota', 'default', 'default'], + ]; + $appValueMap = [ + ['files', 'default_quota', 'none', 'none'], + // do not allow unlimited quota + ['files', 'allow_unlimited_quota', '1', '0'], + ['files', 'quota_preset', '1 GB, 5 GB, 10 GB', '1 GB, 5 GB, 10 GB'], + // expect seeing 1 GB used as fallback value + ['files', 'default_quota', '1 GB', '1 GB'], + ]; + $config->method('getUserValue') + ->willReturnMap($userValueMap); + $config->method('getAppValue') + ->willReturnMap($appValueMap); + + $this->assertEquals('1 GB', $user->getQuota()); + $this->assertEquals(1024 * 1024 * 1024, $user->getQuotaBytes()); + } + + public function testSetQuotaAddressNoChange(): void { + /** + * @var UserInterface | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + /** @var PublicEmitter|MockObject $emitter */ + $emitter = $this->createMock(PublicEmitter::class); + $emitter->expects($this->never()) + ->method('emit'); + + $config = $this->createMock(IConfig::class); + $config->expects($this->any()) + ->method('getUserValue') + ->willReturn('23 TB'); + $config->expects($this->never()) + ->method('setUserValue'); + + $user = new User('foo', $backend, $this->dispatcher, $emitter, $config); + $user->setQuota('23 TB'); + } + + public function testGetLastLogin(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $config = $this->createMock(IConfig::class); + $config->method('getUserValue') + ->willReturnCallback(function ($uid, $app, $key, $default) { + if ($uid === 'foo' && $app === 'login' && $key === 'lastLogin') { + return 42; + } else { + return $default; + } + }); + + $user = new User('foo', $backend, $this->dispatcher, null, $config); + $this->assertSame(42, $user->getLastLogin()); + } + + public function testSetEnabled(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $config = $this->createMock(IConfig::class); + $config->expects($this->once()) + ->method('setUserValue') + ->with( + $this->equalTo('foo'), + $this->equalTo('core'), + $this->equalTo('enabled'), + 'true' + ); + /* dav event listener gets the manager list from config */ + $config->expects(self::any()) + ->method('getUserValue') + ->willReturnCallback( + fn ($user, $app, $key, $default) => ($key === 'enabled' ? 'false' : $default) + ); + + $user = new User('foo', $backend, $this->dispatcher, null, $config); + $user->setEnabled(true); + } + + public function testSetDisabled(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $config = $this->createMock(IConfig::class); + $config->expects($this->once()) + ->method('setUserValue') + ->with( + $this->equalTo('foo'), + $this->equalTo('core'), + $this->equalTo('enabled'), + 'false' + ); + + $user = $this->getMockBuilder(User::class) + ->setConstructorArgs([ + 'foo', + $backend, + $this->dispatcher, + null, + $config, + ]) + ->onlyMethods(['isEnabled', 'triggerChange']) + ->getMock(); + + $user->expects($this->once()) + ->method('isEnabled') + ->willReturn(true); + $user->expects($this->once()) + ->method('triggerChange') + ->with( + 'enabled', + false + ); + + $user->setEnabled(false); + } + + public function testSetDisabledAlreadyDisabled(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $config = $this->createMock(IConfig::class); + $config->expects($this->never()) + ->method('setUserValue'); + + $user = $this->getMockBuilder(User::class) + ->setConstructorArgs([ + 'foo', + $backend, + $this->dispatcher, + null, + $config, + ]) + ->onlyMethods(['isEnabled', 'triggerChange']) + ->getMock(); + + $user->expects($this->once()) + ->method('isEnabled') + ->willReturn(false); + $user->expects($this->never()) + ->method('triggerChange'); + + $user->setEnabled(false); + } + + public function testGetEMailAddress(): void { + /** + * @var Backend | MockObject $backend + */ + $backend = $this->createMock(\Test\Util\User\Dummy::class); + + $config = $this->createMock(IConfig::class); + $config->method('getUserValue') + ->willReturnCallback(function ($uid, $app, $key, $default) { + if ($uid === 'foo' && $app === 'settings' && $key === 'email') { + return 'foo@bar.com'; + } else { + return $default; + } + }); + + $user = new User('foo', $backend, $this->dispatcher, null, $config); + $this->assertSame('foo@bar.com', $user->getEMailAddress()); + } +} |