diff options
Diffstat (limited to 'tests/lib/Authentication')
33 files changed, 5243 insertions, 0 deletions
diff --git a/tests/lib/Authentication/Events/RemoteWipeFinishedTest.php b/tests/lib/Authentication/Events/RemoteWipeFinishedTest.php new file mode 100644 index 00000000000..c89b1e4108f --- /dev/null +++ b/tests/lib/Authentication/Events/RemoteWipeFinishedTest.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Events; + +use OC\Authentication\Events\RemoteWipeFinished; +use OC\Authentication\Token\IToken; +use Test\TestCase; + +class RemoteWipeFinishedTest extends TestCase { + public function testGetToken(): void { + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + + $this->assertSame($token, $event->getToken()); + } +} diff --git a/tests/lib/Authentication/Events/RemoteWipeStartedTest.php b/tests/lib/Authentication/Events/RemoteWipeStartedTest.php new file mode 100644 index 00000000000..fc297f7c087 --- /dev/null +++ b/tests/lib/Authentication/Events/RemoteWipeStartedTest.php @@ -0,0 +1,23 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Events; + +use OC\Authentication\Events\RemoteWipeStarted; +use OC\Authentication\Token\IToken; +use Test\TestCase; + +class RemoteWipeStartedTest extends TestCase { + public function testGetToken(): void { + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + + $this->assertSame($token, $event->getToken()); + } +} diff --git a/tests/lib/Authentication/Listeners/RemoteWipeActivityListenerTest.php b/tests/lib/Authentication/Listeners/RemoteWipeActivityListenerTest.php new file mode 100644 index 00000000000..e2f957ab69a --- /dev/null +++ b/tests/lib/Authentication/Listeners/RemoteWipeActivityListenerTest.php @@ -0,0 +1,137 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Events; + +use OC\Activity\Event as IActivityEvent; +use OC\Authentication\Events\RemoteWipeFinished; +use OC\Authentication\Events\RemoteWipeStarted; +use OC\Authentication\Listeners\RemoteWipeActivityListener; +use OC\Authentication\Token\IToken; +use OCP\Activity\IManager as IActivityManager; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class RemoteWipeActivityListenerTest extends TestCase { + /** @var IActivityManager|MockObject */ + private $activityManager; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var IEventListener */ + private $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->activityManager = $this->createMock(IActivityManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->listener = new RemoteWipeActivityListener( + $this->activityManager, + $this->logger + ); + } + + public function testHandleUnrelated(): void { + $event = new Event(); + + $this->listener->handle($event); + + $this->addToAssertionCount(1); + } + + public function testHandleRemoteWipeStarted(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $activityEvent = $this->createMock(IActivityEvent::class); + $this->activityManager->expects($this->once()) + ->method('generateEvent') + ->willReturn($activityEvent); + $activityEvent->expects($this->once()) + ->method('setApp') + ->with('core') + ->willReturnSelf(); + $activityEvent->expects($this->once()) + ->method('setType') + ->with('security') + ->willReturnSelf(); + $token->method('getUID')->willReturn('user123'); + $activityEvent->expects($this->once()) + ->method('setAuthor') + ->with('user123') + ->willReturnSelf(); + $activityEvent->expects($this->once()) + ->method('setAffectedUser') + ->with('user123') + ->willReturnSelf(); + $token->method('getName')->willReturn('Token 1'); + $activityEvent->expects($this->once()) + ->method('setSubject') + ->with('remote_wipe_start', ['name' => 'Token 1']) + ->willReturnSelf(); + $this->activityManager->expects($this->once()) + ->method('publish'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStartedCanNotPublish(): void { + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $this->activityManager->expects($this->once()) + ->method('generateEvent'); + $this->activityManager->expects($this->once()) + ->method('publish') + ->willThrowException(new \BadMethodCallException()); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinished(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $activityEvent = $this->createMock(IActivityEvent::class); + $this->activityManager->expects($this->once()) + ->method('generateEvent') + ->willReturn($activityEvent); + $activityEvent->expects($this->once()) + ->method('setApp') + ->with('core') + ->willReturnSelf(); + $activityEvent->expects($this->once()) + ->method('setType') + ->with('security') + ->willReturnSelf(); + $token->method('getUID')->willReturn('user123'); + $activityEvent->expects($this->once()) + ->method('setAuthor') + ->with('user123') + ->willReturnSelf(); + $activityEvent->expects($this->once()) + ->method('setAffectedUser') + ->with('user123') + ->willReturnSelf(); + $token->method('getName')->willReturn('Token 1'); + $activityEvent->expects($this->once()) + ->method('setSubject') + ->with('remote_wipe_finish', ['name' => 'Token 1']) + ->willReturnSelf(); + $this->activityManager->expects($this->once()) + ->method('publish'); + + $this->listener->handle($event); + } +} diff --git a/tests/lib/Authentication/Listeners/RemoteWipeEmailListenerTest.php b/tests/lib/Authentication/Listeners/RemoteWipeEmailListenerTest.php new file mode 100644 index 00000000000..1c45add4e31 --- /dev/null +++ b/tests/lib/Authentication/Listeners/RemoteWipeEmailListenerTest.php @@ -0,0 +1,225 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\Authentication\Listeners; + +use Exception; +use OC\Authentication\Events\RemoteWipeFinished; +use OC\Authentication\Events\RemoteWipeStarted; +use OC\Authentication\Listeners\RemoteWipeEmailListener; +use OC\Authentication\Token\IToken; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IL10N; +use OCP\IUser; +use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Mail\IMailer; +use OCP\Mail\IMessage; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class RemoteWipeEmailListenerTest extends TestCase { + /** @var IMailer|MockObject */ + private $mailer; + + /** @var IUserManager|MockObject */ + private $userManager; + + /** @var IFactory|MockObject */ + private $l10nFactory; + + /** @var IL10N|MockObject */ + private $l10n; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var IEventListener */ + private $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->mailer = $this->createMock(IMailer::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->l10nFactory = $this->createMock(IFactory::class); + $this->l10n = $this->createMock(IL10N::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->l10nFactory->method('get')->with('core')->willReturn($this->l10n); + $this->l10n->method('t')->willReturnArgument(0); + + $this->listener = new RemoteWipeEmailListener( + $this->mailer, + $this->userManager, + $this->l10nFactory, + $this->logger + ); + } + + + public function testHandleUnrelated(): void { + $event = new Event(); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStartedInvalidUser(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $token->method('getUID')->willReturn('nope'); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn(null); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStartedNoEmailSet(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn(null); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStartedTransmissionError(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn('user@domain.org'); + $this->mailer->expects($this->once()) + ->method('send') + ->willThrowException(new Exception()); + $this->logger->expects($this->once()) + ->method('error'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeStarted(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn('user@domain.org'); + $message = $this->createMock(IMessage::class); + $this->mailer->expects($this->once()) + ->method('createMessage') + ->willReturn($message); + $message->expects($this->once()) + ->method('setTo') + ->with($this->equalTo(['user@domain.org'])); + $this->mailer->expects($this->once()) + ->method('send') + ->with($message); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinishedInvalidUser(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $token->method('getUID')->willReturn('nope'); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn(null); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinishedNoEmailSet(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn(null); + $this->mailer->expects($this->never())->method('send'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinishedTransmissionError(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn('user@domain.org'); + $this->mailer->expects($this->once()) + ->method('send') + ->willThrowException(new Exception()); + $this->logger->expects($this->once()) + ->method('error'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinished(): void { + /** @var IToken|MockObject $token */ + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $token->method('getUID')->willReturn('nope'); + $user = $this->createMock(IUser::class); + $this->userManager->expects($this->once()) + ->method('get') + ->with('nope') + ->willReturn($user); + $user->method('getEMailAddress')->willReturn('user@domain.org'); + $message = $this->createMock(IMessage::class); + $this->mailer->expects($this->once()) + ->method('createMessage') + ->willReturn($message); + $message->expects($this->once()) + ->method('setTo') + ->with($this->equalTo(['user@domain.org'])); + $this->mailer->expects($this->once()) + ->method('send') + ->with($message); + + $this->listener->handle($event); + } +} diff --git a/tests/lib/Authentication/Listeners/RemoteWipeNotificationsListenerTest.php b/tests/lib/Authentication/Listeners/RemoteWipeNotificationsListenerTest.php new file mode 100644 index 00000000000..53fa502b199 --- /dev/null +++ b/tests/lib/Authentication/Listeners/RemoteWipeNotificationsListenerTest.php @@ -0,0 +1,134 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Events; + +use DateTime; +use OC\Authentication\Events\RemoteWipeFinished; +use OC\Authentication\Events\RemoteWipeStarted; +use OC\Authentication\Listeners\RemoteWipeNotificationsListener; +use OC\Authentication\Token\IToken; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class RemoteWipeNotificationsListenerTest extends TestCase { + /** @var INotificationManager|MockObject */ + private $notificationManager; + + /** @var ITimeFactory|MockObject */ + private $timeFactory; + + /** @var IEventListener */ + private $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->notificationManager = $this->createMock(INotificationManager::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->listener = new RemoteWipeNotificationsListener( + $this->notificationManager, + $this->timeFactory + ); + } + + public function testHandleUnrelated(): void { + $event = new Event(); + + $this->listener->handle($event); + + $this->addToAssertionCount(1); + } + + public function testHandleRemoteWipeStarted(): void { + $token = $this->createMock(IToken::class); + $event = new RemoteWipeStarted($token); + $notification = $this->createMock(INotification::class); + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + $notification->expects($this->once()) + ->method('setApp') + ->with('auth') + ->willReturnSelf(); + $token->method('getUID')->willReturn('user123'); + $notification->expects($this->once()) + ->method('setUser') + ->with('user123') + ->willReturnSelf(); + $now = new DateTime(); + $this->timeFactory->method('getDateTime')->willReturn($now); + $notification->expects($this->once()) + ->method('setDateTime') + ->with($now) + ->willReturnSelf(); + $token->method('getId')->willReturn(123); + $notification->expects($this->once()) + ->method('setObject') + ->with('token', '123') + ->willReturnSelf(); + $token->method('getName')->willReturn('Token 1'); + $notification->expects($this->once()) + ->method('setSubject') + ->with('remote_wipe_start', [ + 'name' => 'Token 1' + ]) + ->willReturnSelf(); + $this->notificationManager->expects($this->once()) + ->method('notify'); + + $this->listener->handle($event); + } + + public function testHandleRemoteWipeFinished(): void { + $token = $this->createMock(IToken::class); + $event = new RemoteWipeFinished($token); + $notification = $this->createMock(INotification::class); + $this->notificationManager->expects($this->once()) + ->method('createNotification') + ->willReturn($notification); + $notification->expects($this->once()) + ->method('setApp') + ->with('auth') + ->willReturnSelf(); + $token->method('getUID')->willReturn('user123'); + $notification->expects($this->once()) + ->method('setUser') + ->with('user123') + ->willReturnSelf(); + $now = new DateTime(); + $this->timeFactory->method('getDateTime')->willReturn($now); + $notification->expects($this->once()) + ->method('setDateTime') + ->with($now) + ->willReturnSelf(); + $token->method('getId')->willReturn(123); + $notification->expects($this->once()) + ->method('setObject') + ->with('token', '123') + ->willReturnSelf(); + $token->method('getName')->willReturn('Token 1'); + $notification->expects($this->once()) + ->method('setSubject') + ->with('remote_wipe_finish', [ + 'name' => 'Token 1' + ]) + ->willReturnSelf(); + $this->notificationManager->expects($this->once()) + ->method('notify'); + + $this->listener->handle($event); + } +} diff --git a/tests/lib/Authentication/Listeners/UserDeletedTokenCleanupListenerTest.php b/tests/lib/Authentication/Listeners/UserDeletedTokenCleanupListenerTest.php new file mode 100644 index 00000000000..1861a5b2150 --- /dev/null +++ b/tests/lib/Authentication/Listeners/UserDeletedTokenCleanupListenerTest.php @@ -0,0 +1,103 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Listeners; + +use Exception; +use OC\Authentication\Listeners\UserDeletedTokenCleanupListener; +use OC\Authentication\Token\IToken; +use OC\Authentication\Token\Manager; +use OCP\EventDispatcher\Event; +use OCP\IUser; +use OCP\User\Events\UserDeletedEvent; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class UserDeletedTokenCleanupListenerTest extends TestCase { + /** @var Manager|MockObject */ + private $manager; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var UserDeletedTokenCleanupListener */ + private $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->manager = $this->createMock(Manager::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->listener = new UserDeletedTokenCleanupListener( + $this->manager, + $this->logger + ); + } + + public function testHandleUnrelated(): void { + $event = new Event(); + $this->manager->expects($this->never())->method('getTokenByUser'); + $this->logger->expects($this->never())->method('error'); + + $this->listener->handle($event); + } + + public function testHandleWithErrors(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $event = new UserDeletedEvent($user); + $exception = new Exception('nope'); + $this->manager->expects($this->once()) + ->method('getTokenByUser') + ->with('user123') + ->willThrowException($exception); + $this->logger->expects($this->once()) + ->method('error'); + + $this->listener->handle($event); + } + + public function testHandle(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $event = new UserDeletedEvent($user); + $token1 = $this->createMock(IToken::class); + $token1->method('getId')->willReturn(1); + $token2 = $this->createMock(IToken::class); + $token2->method('getId')->willReturn(2); + $token3 = $this->createMock(IToken::class); + $token3->method('getId')->willReturn(3); + $this->manager->expects($this->once()) + ->method('getTokenByUser') + ->with('user123') + ->willReturn([ + $token1, + $token2, + $token3, + ]); + + $calls = [ + ['user123', 1], + ['user123', 2], + ['user123', 3], + ]; + $this->manager->expects($this->exactly(3)) + ->method('invalidateTokenById') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + $this->logger->expects($this->never()) + ->method('error'); + + $this->listener->handle($event); + } +} diff --git a/tests/lib/Authentication/Login/ALoginTestCommand.php b/tests/lib/Authentication/Login/ALoginTestCommand.php new file mode 100644 index 00000000000..b955b20beba --- /dev/null +++ b/tests/lib/Authentication/Login/ALoginTestCommand.php @@ -0,0 +1,104 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\LoginData; +use OCP\IRequest; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +abstract class ALoginTestCommand extends TestCase { + /** @var IRequest|MockObject */ + protected $request; + + /** @var string */ + protected $username = 'user123'; + + /** @var string */ + protected $password = '123456'; + + /** @var string */ + protected $redirectUrl = '/apps/contacts'; + + /** @var string */ + protected $timezone = 'Europe/Vienna'; + + protected $timeZoneOffset = '2'; + + /** @var IUser|MockObject */ + protected $user; + + /** @var ALoginTestCommand */ + protected $cmd; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->user = $this->createMock(IUser::class); + } + + protected function getBasicLoginData(): LoginData { + return new LoginData( + $this->request, + $this->username, + $this->password + ); + } + + protected function getInvalidLoginData(): LoginData { + return new LoginData( + $this->request, + $this->username, + $this->password + ); + } + + protected function getFailedLoginData(): LoginData { + $data = new LoginData( + $this->request, + $this->username, + $this->password + ); + $data->setUser(false); + return $data; + } + + protected function getLoggedInLoginData(): LoginData { + $basic = $this->getBasicLoginData(); + $basic->setUser($this->user); + return $basic; + } + + protected function getLoggedInLoginDataWithRedirectUrl(): LoginData { + $data = new LoginData( + $this->request, + $this->username, + $this->password, + $this->redirectUrl + ); + $data->setUser($this->user); + return $data; + } + + protected function getLoggedInLoginDataWithTimezone(): LoginData { + $data = new LoginData( + $this->request, + $this->username, + $this->password, + null, + $this->timezone, + $this->timeZoneOffset + ); + $data->setUser($this->user); + return $data; + } +} diff --git a/tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php b/tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php new file mode 100644 index 00000000000..5ff2da28946 --- /dev/null +++ b/tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php @@ -0,0 +1,47 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\ClearLostPasswordTokensCommand; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; + +class ClearLostPasswordTokensCommandTest extends ALoginTestCommand { + /** @var IConfig|MockObject */ + private $config; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + + $this->cmd = new ClearLostPasswordTokensCommand( + $this->config + ); + } + + public function testProcess(): void { + $data = $this->getLoggedInLoginData(); + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn($this->username); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with( + $this->username, + 'core', + 'lostpassword' + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } +} diff --git a/tests/lib/Authentication/Login/CompleteLoginCommandTest.php b/tests/lib/Authentication/Login/CompleteLoginCommandTest.php new file mode 100644 index 00000000000..5b08368671f --- /dev/null +++ b/tests/lib/Authentication/Login/CompleteLoginCommandTest.php @@ -0,0 +1,48 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\CompleteLoginCommand; +use OC\User\Session; +use PHPUnit\Framework\MockObject\MockObject; + +class CompleteLoginCommandTest extends ALoginTestCommand { + /** @var Session|MockObject */ + private $session; + + protected function setUp(): void { + parent::setUp(); + + $this->session = $this->createMock(Session::class); + + $this->cmd = new CompleteLoginCommand( + $this->session + ); + } + + public function testProcess(): void { + $data = $this->getLoggedInLoginData(); + $this->session->expects($this->once()) + ->method('completeLogin') + ->with( + $this->user, + $this->equalTo( + [ + 'loginName' => $this->username, + 'password' => $this->password, + ] + ) + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } +} diff --git a/tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php b/tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php new file mode 100644 index 00000000000..668c0a6d6ea --- /dev/null +++ b/tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php @@ -0,0 +1,103 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\CreateSessionTokenCommand; +use OC\Authentication\Token\IToken; +use OC\User\Session; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; + +class CreateSessionTokenCommandTest extends ALoginTestCommand { + /** @var IConfig|MockObject */ + private $config; + + /** @var Session|MockObject */ + private $userSession; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->userSession = $this->createMock(Session::class); + + $this->cmd = new CreateSessionTokenCommand( + $this->config, + $this->userSession + ); + } + + public function testProcess(): void { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->once()) + ->method('getSystemValueInt') + ->with( + 'remember_login_cookie_lifetime', + 60 * 60 * 24 * 15 + ) + ->willReturn(100); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn($this->username); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with( + $this->request, + $this->username, + $this->username, + $this->password, + IToken::REMEMBER + ); + $this->userSession->expects($this->once()) + ->method('updateTokens') + ->with( + $this->username, + $this->password + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessDoNotRemember(): void { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->once()) + ->method('getSystemValueInt') + ->with( + 'remember_login_cookie_lifetime', + 60 * 60 * 24 * 15 + ) + ->willReturn(0); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn($this->username); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with( + $this->request, + $this->username, + $this->username, + $this->password, + IToken::DO_NOT_REMEMBER + ); + $this->userSession->expects($this->once()) + ->method('updateTokens') + ->with( + $this->username, + $this->password + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertFalse($data->isRememberLogin()); + } +} diff --git a/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php b/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php new file mode 100644 index 00000000000..499abafa6e3 --- /dev/null +++ b/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php @@ -0,0 +1,74 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\FinishRememberedLoginCommand; +use OC\User\Session; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; + +class FinishRememberedLoginCommandTest extends ALoginTestCommand { + /** @var Session|MockObject */ + private $userSession; + /** @var IConfig|MockObject */ + private $config; + + protected function setUp(): void { + parent::setUp(); + + $this->userSession = $this->createMock(Session::class); + $this->config = $this->createMock(IConfig::class); + + $this->cmd = new FinishRememberedLoginCommand( + $this->userSession, + $this->config + ); + } + + public function testProcessNotRememberedLogin(): void { + $data = $this->getLoggedInLoginData(); + $data->setRememberLogin(false); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcess(): void { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->once()) + ->method('getSystemValueBool') + ->with('auto_logout', false) + ->willReturn(false); + $this->userSession->expects($this->once()) + ->method('createRememberMeToken') + ->with($this->user); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessNotRemeberedLoginWithAutologout(): void { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->once()) + ->method('getSystemValueBool') + ->with('auto_logout', false) + ->willReturn(true); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } +} diff --git a/tests/lib/Authentication/Login/LoggedInCheckCommandTest.php b/tests/lib/Authentication/Login/LoggedInCheckCommandTest.php new file mode 100644 index 00000000000..7b011d70673 --- /dev/null +++ b/tests/lib/Authentication/Login/LoggedInCheckCommandTest.php @@ -0,0 +1,55 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\LoggedInCheckCommand; +use OC\Core\Controller\LoginController; +use OCP\EventDispatcher\IEventDispatcher; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +class LoggedInCheckCommandTest extends ALoginTestCommand { + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var IEventDispatcher|MockObject */ + private $dispatcher; + + protected function setUp(): void { + parent::setUp(); + + $this->logger = $this->createMock(LoggerInterface::class); + $this->dispatcher = $this->createMock(IEventDispatcher::class); + + $this->cmd = new LoggedInCheckCommand( + $this->logger, + $this->dispatcher + ); + } + + public function testProcessSuccessfulLogin(): void { + $data = $this->getLoggedInLoginData(); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessFailedLogin(): void { + $data = $this->getFailedLoginData(); + $this->logger->expects($this->once()) + ->method('warning'); + + $result = $this->cmd->process($data); + + $this->assertFalse($result->isSuccess()); + $this->assertSame(LoginController::LOGIN_MSG_INVALIDPASSWORD, $result->getErrorMessage()); + } +} diff --git a/tests/lib/Authentication/Login/PreLoginHookCommandTest.php b/tests/lib/Authentication/Login/PreLoginHookCommandTest.php new file mode 100644 index 00000000000..0e5096baf55 --- /dev/null +++ b/tests/lib/Authentication/Login/PreLoginHookCommandTest.php @@ -0,0 +1,48 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\PreLoginHookCommand; +use OC\User\Manager; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; + +class PreLoginHookCommandTest extends ALoginTestCommand { + /** @var IUserManager|MockObject */ + private $userManager; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(Manager::class); + + $this->cmd = new PreLoginHookCommand( + $this->userManager + ); + } + + public function testProcess(): void { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('emit') + ->with( + '\OC\User', + 'preLogin', + [ + $this->username, + $this->password, + ] + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } +} diff --git a/tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php b/tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php new file mode 100644 index 00000000000..fb8240c4b1e --- /dev/null +++ b/tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php @@ -0,0 +1,72 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\SetUserTimezoneCommand; +use OCP\IConfig; +use OCP\ISession; +use PHPUnit\Framework\MockObject\MockObject; + +class SetUserTimezoneCommandTest extends ALoginTestCommand { + /** @var IConfig|MockObject */ + private $config; + + /** @var ISession|MockObject */ + private $session; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->session = $this->createMock(ISession::class); + + $this->cmd = new SetUserTimezoneCommand( + $this->config, + $this->session + ); + } + + public function testProcessNoTimezoneSet(): void { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->never()) + ->method('setUserValue'); + $this->session->expects($this->never()) + ->method('set'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcess(): void { + $data = $this->getLoggedInLoginDataWithTimezone(); + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn($this->username); + $this->config->expects($this->once()) + ->method('setUserValue') + ->with( + $this->username, + 'core', + 'timezone', + $this->timezone + ); + $this->session->expects($this->once()) + ->method('set') + ->with( + 'timezone', + $this->timeZoneOffset + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } +} diff --git a/tests/lib/Authentication/Login/TwoFactorCommandTest.php b/tests/lib/Authentication/Login/TwoFactorCommandTest.php new file mode 100644 index 00000000000..a95e4b50cbc --- /dev/null +++ b/tests/lib/Authentication/Login/TwoFactorCommandTest.php @@ -0,0 +1,341 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\TwoFactorCommand; +use OC\Authentication\TwoFactorAuth\Manager; +use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; +use OC\Authentication\TwoFactorAuth\ProviderSet; +use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; +use OCP\Authentication\TwoFactorAuth\IProvider as ITwoFactorAuthProvider; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; + +class TwoFactorCommandTest extends ALoginTestCommand { + /** @var Manager|MockObject */ + private $twoFactorManager; + + /** @var MandatoryTwoFactor|MockObject */ + private $mandatoryTwoFactor; + + /** @var IURLGenerator|MockObject */ + private $urlGenerator; + + protected function setUp(): void { + parent::setUp(); + + $this->twoFactorManager = $this->createMock(Manager::class); + $this->mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + $this->cmd = new TwoFactorCommand( + $this->twoFactorManager, + $this->mandatoryTwoFactor, + $this->urlGenerator + ); + } + + public function testNotTwoFactorAuthenticated(): void { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(false); + $this->twoFactorManager->expects($this->never()) + ->method('prepareTwoFactorLogin'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessOneActiveProvider(): void { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider = $this->createMock(ITwoFactorAuthProvider::class); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider, + ], false)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $provider->expects($this->once()) + ->method('getId') + ->willReturn('test'); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.showChallenge', + [ + 'challengeProviderId' => 'test' + ] + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessMissingProviders(): void { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider = $this->createMock(ITwoFactorAuthProvider::class); + $provider->expects($this->once()) + ->method('getId') + ->willReturn('test1'); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider, + ], true)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessTwoActiveProviders(): void { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider1 = $this->createMock(ITwoFactorAuthProvider::class); + $provider2 = $this->createMock(ITwoFactorAuthProvider::class); + $provider1->expects($this->once()) + ->method('getId') + ->willReturn('test1'); + $provider2->expects($this->once()) + ->method('getId') + ->willReturn('test2'); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider1, + $provider2, + ], false)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessFailingProviderAndEnforcedButNoSetupProviders(): void { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([], true)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(true); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessFailingProviderAndEnforced(): void { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider = $this->createMock(IActivatableAtLogin::class); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider, + ], true)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(true); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessNoProvidersButEnforced(): void { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([], false)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(true); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessWithRedirectUrl(): void { + $data = $this->getLoggedInLoginDataWithRedirectUrl(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider = $this->createMock(ITwoFactorAuthProvider::class); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider, + ], false)); + $this->twoFactorManager->expects($this->once()) + ->method('getLoginSetupProviders') + ->with($this->user) + ->willReturn([]); + $this->mandatoryTwoFactor->expects($this->any()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $provider->expects($this->once()) + ->method('getId') + ->willReturn('test'); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.showChallenge', + [ + 'challengeProviderId' => 'test', + 'redirect_url' => $this->redirectUrl, + ] + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } +} diff --git a/tests/lib/Authentication/Login/UidLoginCommandTest.php b/tests/lib/Authentication/Login/UidLoginCommandTest.php new file mode 100644 index 00000000000..daae34e2212 --- /dev/null +++ b/tests/lib/Authentication/Login/UidLoginCommandTest.php @@ -0,0 +1,61 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\UidLoginCommand; +use OC\User\Manager; +use PHPUnit\Framework\MockObject\MockObject; + +class UidLoginCommandTest extends ALoginTestCommand { + /** @var Manager|MockObject */ + private $userManager; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(Manager::class); + + $this->cmd = new UidLoginCommand( + $this->userManager + ); + } + + public function testProcessFailingLogin(): void { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('checkPasswordNoLogging') + ->with( + $this->username, + $this->password + ) + ->willReturn(false); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertFalse($data->getUser()); + } + + public function testProcess(): void { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('checkPasswordNoLogging') + ->with( + $this->username, + $this->password + ) + ->willReturn($this->user); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals($this->user, $data->getUser()); + } +} diff --git a/tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php b/tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php new file mode 100644 index 00000000000..1a845a05c23 --- /dev/null +++ b/tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php @@ -0,0 +1,46 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\UpdateLastPasswordConfirmCommand; +use OCP\ISession; +use PHPUnit\Framework\MockObject\MockObject; + +class UpdateLastPasswordConfirmCommandTest extends ALoginTestCommand { + /** @var ISession|MockObject */ + private $session; + + protected function setUp(): void { + parent::setUp(); + + $this->session = $this->createMock(ISession::class); + + $this->cmd = new UpdateLastPasswordConfirmCommand( + $this->session + ); + } + + public function testProcess(): void { + $data = $this->getLoggedInLoginData(); + $this->user->expects($this->once()) + ->method('getLastLogin') + ->willReturn(1234); + $this->session->expects($this->once()) + ->method('set') + ->with( + 'last-password-confirm', + 1234 + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } +} diff --git a/tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php b/tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php new file mode 100644 index 00000000000..ee4e171d443 --- /dev/null +++ b/tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php @@ -0,0 +1,79 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use OC\Authentication\Login\UserDisabledCheckCommand; +use OC\Core\Controller\LoginController; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; + +class UserDisabledCheckCommandTest extends ALoginTestCommand { + /** @var IUserManager|MockObject */ + private $userManager; + + /** @var LoggerInterface|MockObject */ + private $logger; + + protected function setUp(): void { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->cmd = new UserDisabledCheckCommand( + $this->userManager, + $this->logger + ); + } + + public function testProcessNonExistingUser(): void { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('get') + ->with($this->username) + ->willReturn(null); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessDisabledUser(): void { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('get') + ->with($this->username) + ->willReturn($this->user); + $this->user->expects($this->once()) + ->method('isEnabled') + ->willReturn(false); + + $result = $this->cmd->process($data); + + $this->assertFalse($result->isSuccess()); + $this->assertSame(LoginController::LOGIN_MSG_USERDISABLED, $result->getErrorMessage()); + } + + public function testProcess(): void { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('get') + ->with($this->username) + ->willReturn($this->user); + $this->user->expects($this->once()) + ->method('isEnabled') + ->willReturn(true); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } +} diff --git a/tests/lib/Authentication/LoginCredentials/CredentialsTest.php b/tests/lib/Authentication/LoginCredentials/CredentialsTest.php new file mode 100644 index 00000000000..a64ad6c9b76 --- /dev/null +++ b/tests/lib/Authentication/LoginCredentials/CredentialsTest.php @@ -0,0 +1,47 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\LoginCredentials; + +use OC\Authentication\LoginCredentials\Credentials; +use Test\TestCase; + +class CredentialsTest extends TestCase { + /** @var string */ + private $uid; + + /** @var string */ + private $user; + + /** @var string */ + private $password; + + /** @var Credentials */ + private $credentials; + + protected function setUp(): void { + parent::setUp(); + + $this->uid = 'user123'; + $this->user = 'User123'; + $this->password = '123456'; + + $this->credentials = new Credentials($this->uid, $this->user, $this->password); + } + + public function testGetUID(): void { + $this->assertEquals($this->uid, $this->credentials->getUID()); + } + + public function testGetUserName(): void { + $this->assertEquals($this->user, $this->credentials->getLoginName()); + } + + public function testGetPassword(): void { + $this->assertEquals($this->password, $this->credentials->getPassword()); + } +} diff --git a/tests/lib/Authentication/LoginCredentials/StoreTest.php b/tests/lib/Authentication/LoginCredentials/StoreTest.php new file mode 100644 index 00000000000..aca586b91ec --- /dev/null +++ b/tests/lib/Authentication/LoginCredentials/StoreTest.php @@ -0,0 +1,296 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\LoginCredentials; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\LoginCredentials\Credentials; +use OC\Authentication\LoginCredentials\Store; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; +use OCP\Authentication\Exceptions\CredentialsUnavailableException; +use OCP\ISession; +use OCP\Security\ICrypto; +use OCP\Session\Exceptions\SessionNotAvailableException; +use Psr\Log\LoggerInterface; +use Test\TestCase; +use function json_encode; + +class StoreTest extends TestCase { + /** @var ISession|\PHPUnit\Framework\MockObject\MockObject */ + private $session; + + /** @var IProvider|\PHPUnit\Framework\MockObject\MockObject */ + private $tokenProvider; + + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + /** @var ICrypto|\PHPUnit\Framework\MockObject\MockObject */ + private $crypto; + + /** @var Store */ + private $store; + + protected function setUp(): void { + parent::setUp(); + + $this->session = $this->createMock(ISession::class); + $this->tokenProvider = $this->createMock(IProvider::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->crypto = $this->createMock(ICrypto::class); + + $this->store = new Store($this->session, $this->logger, $this->crypto, $this->tokenProvider); + } + + public function testAuthenticate(): void { + $params = [ + 'run' => true, + 'uid' => 'user123', + 'password' => '123456', + ]; + + $this->session->expects($this->once()) + ->method('set') + ->with($this->equalTo('login_credentials'), $this->equalTo(json_encode($params))); + $this->crypto->expects($this->once()) + ->method('encrypt') + ->willReturn('123456'); + + $this->store->authenticate($params); + } + + public function testSetSession(): void { + $session = $this->createMock(ISession::class); + + $this->store->setSession($session); + $this->addToAssertionCount(1); + } + + public function testGetLoginCredentialsNoTokenProvider(): void { + $this->store = new Store($this->session, $this->logger, $this->crypto, null); + + $this->expectException(CredentialsUnavailableException::class); + + $this->store->getLoginCredentials(); + } + + public function testGetLoginCredentials(): void { + $uid = 'uid'; + $user = 'user123'; + $password = 'passme'; + $token = $this->createMock(IToken::class); + $this->session->expects($this->once()) + ->method('getId') + ->willReturn('sess2233'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sess2233') + ->willReturn($token); + $token->expects($this->once()) + ->method('getUID') + ->willReturn($uid); + $token->expects($this->once()) + ->method('getLoginName') + ->willReturn($user); + $this->tokenProvider->expects($this->once()) + ->method('getPassword') + ->with($token, 'sess2233') + ->willReturn($password); + $expected = new Credentials($uid, $user, $password); + + $creds = $this->store->getLoginCredentials(); + + $this->assertEquals($expected, $creds); + } + + public function testGetLoginCredentialsSessionNotAvailable(): void { + $this->session->expects($this->once()) + ->method('getId') + ->willThrowException(new SessionNotAvailableException()); + $this->expectException(CredentialsUnavailableException::class); + + $this->store->getLoginCredentials(); + } + + public function testGetLoginCredentialsInvalidToken(): void { + $this->session->expects($this->once()) + ->method('getId') + ->willReturn('sess2233'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sess2233') + ->willThrowException(new InvalidTokenException()); + $this->expectException(CredentialsUnavailableException::class); + + $this->store->getLoginCredentials(); + } + + public function testGetLoginCredentialsPartialCredentialsAndSessionName(): void { + $uid = 'id987'; + $user = 'user987'; + $password = '7389374'; + + $this->session->expects($this->once()) + ->method('getId') + ->willReturn('sess2233'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sess2233') + ->willThrowException(new InvalidTokenException()); + $this->session->expects($this->once()) + ->method('exists') + ->with($this->equalTo('login_credentials')) + ->willReturn(true); + $this->crypto->expects($this->once()) + ->method('decrypt') + ->willReturn($password); + $this->session->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + [ + 'login_credentials', + json_encode([ + 'uid' => $uid, + 'password' => $password, + ]) + ], + [ + 'loginname', + $user, + ], + ]); + $expected = new Credentials($uid, $user, $password); + + $actual = $this->store->getLoginCredentials(); + + $this->assertEquals($expected, $actual); + } + + public function testGetLoginCredentialsPartialCredentials(): void { + $uid = 'id987'; + $password = '7389374'; + + $this->session->expects($this->once()) + ->method('getId') + ->willReturn('sess2233'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sess2233') + ->willThrowException(new InvalidTokenException()); + $this->session->expects($this->once()) + ->method('exists') + ->with($this->equalTo('login_credentials')) + ->willReturn(true); + $this->crypto->expects($this->once()) + ->method('decrypt') + ->willReturn($password); + $this->session->expects($this->exactly(2)) + ->method('get') + ->willReturnMap([ + [ + 'login_credentials', + json_encode([ + 'uid' => $uid, + 'password' => $password, + ]) + ], + [ + 'loginname', + null, + ], + ]); + $expected = new Credentials($uid, $uid, $password); + + $actual = $this->store->getLoginCredentials(); + + $this->assertEquals($expected, $actual); + } + + public function testGetLoginCredentialsInvalidTokenLoginCredentials(): void { + $uid = 'id987'; + $user = 'user987'; + $password = '7389374'; + + $this->session->expects($this->once()) + ->method('getId') + ->willReturn('sess2233'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sess2233') + ->willThrowException(new InvalidTokenException()); + $this->session->expects($this->once()) + ->method('exists') + ->with($this->equalTo('login_credentials')) + ->willReturn(true); + $this->crypto->expects($this->once()) + ->method('decrypt') + ->willReturn($password); + $this->session->expects($this->once()) + ->method('get') + ->with($this->equalTo('login_credentials')) + ->willReturn('{"run":true,"uid":"id987","loginName":"user987","password":"7389374"}'); + $expected = new Credentials($uid, $user, $password); + + $actual = $this->store->getLoginCredentials(); + + $this->assertEquals($expected, $actual); + } + + public function testGetLoginCredentialsPasswordlessToken(): void { + $this->session->expects($this->once()) + ->method('getId') + ->willReturn('sess2233'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sess2233') + ->willThrowException(new PasswordlessTokenException()); + $this->expectException(CredentialsUnavailableException::class); + + $this->store->getLoginCredentials(); + } + + public function testAuthenticatePasswordlessToken(): void { + $user = 'user987'; + $password = null; + + $params = [ + 'run' => true, + 'loginName' => $user, + 'uid' => $user, + 'password' => $password, + ]; + + $this->session->expects($this->once()) + ->method('set') + ->with($this->equalTo('login_credentials'), $this->equalTo(json_encode($params))); + + + $this->session->expects($this->once()) + ->method('getId') + ->willReturn('sess2233'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sess2233') + ->willThrowException(new PasswordlessTokenException()); + + $this->session->expects($this->once()) + ->method('exists') + ->with($this->equalTo('login_credentials')) + ->willReturn(true); + $this->session->expects($this->once()) + ->method('get') + ->with($this->equalTo('login_credentials')) + ->willReturn(json_encode($params)); + + $this->store->authenticate($params); + $actual = $this->store->getLoginCredentials(); + + $expected = new Credentials($user, $user, $password); + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/lib/Authentication/Token/ManagerTest.php b/tests/lib/Authentication/Token/ManagerTest.php new file mode 100644 index 00000000000..58bbe236248 --- /dev/null +++ b/tests/lib/Authentication/Token/ManagerTest.php @@ -0,0 +1,406 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Token; + +use Doctrine\DBAL\Exception\UniqueConstraintViolationException; +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\IToken; +use OC\Authentication\Token\Manager; +use OC\Authentication\Token\PublicKeyToken; +use OC\Authentication\Token\PublicKeyTokenProvider; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ManagerTest extends TestCase { + /** @var PublicKeyTokenProvider|MockObject */ + private $publicKeyTokenProvider; + /** @var Manager */ + private $manager; + + protected function setUp(): void { + parent::setUp(); + + $this->publicKeyTokenProvider = $this->createMock(PublicKeyTokenProvider::class); + $this->manager = new Manager( + $this->publicKeyTokenProvider + ); + } + + public function testGenerateToken(): void { + $token = new PublicKeyToken(); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('generateToken') + ->with( + 'token', + 'uid', + 'loginName', + 'password', + 'name', + IToken::TEMPORARY_TOKEN, + IToken::REMEMBER + )->willReturn($token); + + $actual = $this->manager->generateToken( + 'token', + 'uid', + 'loginName', + 'password', + 'name', + IToken::TEMPORARY_TOKEN, + IToken::REMEMBER + ); + + $this->assertSame($token, $actual); + } + + public function testGenerateConflictingToken(): void { + /** @var MockObject|UniqueConstraintViolationException $exception */ + $exception = $this->createMock(UniqueConstraintViolationException::class); + + $token = new PublicKeyToken(); + $token->setUid('uid'); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('generateToken') + ->with( + 'token', + 'uid', + 'loginName', + 'password', + 'name', + IToken::TEMPORARY_TOKEN, + IToken::REMEMBER + )->willThrowException($exception); + $this->publicKeyTokenProvider->expects($this->once()) + ->method('getToken') + ->with('token') + ->willReturn($token); + + $actual = $this->manager->generateToken( + 'token', + 'uid', + 'loginName', + 'password', + 'name', + IToken::TEMPORARY_TOKEN, + IToken::REMEMBER + ); + + $this->assertSame($token, $actual); + } + + public function testGenerateTokenTooLongName(): void { + $token = $this->createMock(IToken::class); + $token->method('getName') + ->willReturn(str_repeat('a', 120) . '…'); + + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('generateToken') + ->with( + 'token', + 'uid', + 'loginName', + 'password', + str_repeat('a', 120) . '…', + IToken::TEMPORARY_TOKEN, + IToken::REMEMBER + )->willReturn($token); + + $actual = $this->manager->generateToken( + 'token', + 'uid', + 'loginName', + 'password', + str_repeat('a', 200), + IToken::TEMPORARY_TOKEN, + IToken::REMEMBER + ); + + $this->assertSame(121, mb_strlen($actual->getName())); + } + + public static function tokenData(): array { + return [ + [new PublicKeyToken()], + [IToken::class], + ]; + } + + protected function setNoCall(IToken $token) { + if (!($token instanceof PublicKeyToken)) { + $this->publicKeyTokenProvider->expects($this->never()) + ->method($this->anything()); + } + } + + protected function setCall(IToken $token, string $function, $return = null) { + if ($token instanceof PublicKeyToken) { + $this->publicKeyTokenProvider->expects($this->once()) + ->method($function) + ->with($token) + ->willReturn($return); + } + } + + protected function setException(IToken $token) { + if (!($token instanceof PublicKeyToken)) { + $this->expectException(InvalidTokenException::class); + } + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tokenData')] + public function testUpdateToken(IToken|string $token): void { + if (is_string($token)) { + $token = $this->createMock($token); + } + + $this->setNoCall($token); + $this->setCall($token, 'updateToken'); + $this->setException($token); + + $this->manager->updateToken($token); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tokenData')] + public function testUpdateTokenActivity(IToken|string $token): void { + if (is_string($token)) { + $token = $this->createMock($token); + } + + $this->setNoCall($token); + $this->setCall($token, 'updateTokenActivity'); + $this->setException($token); + + $this->manager->updateTokenActivity($token); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tokenData')] + public function testGetPassword(IToken|string $token): void { + if (is_string($token)) { + $token = $this->createMock($token); + } + + $this->setNoCall($token); + $this->setCall($token, 'getPassword', 'password'); + $this->setException($token); + + $result = $this->manager->getPassword($token, 'tokenId', 'password'); + + $this->assertSame('password', $result); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('tokenData')] + public function testSetPassword(IToken|string $token): void { + if (is_string($token)) { + $token = $this->createMock($token); + } + + $this->setNoCall($token); + $this->setCall($token, 'setPassword'); + $this->setException($token); + + $this->manager->setPassword($token, 'tokenId', 'password'); + } + + public function testInvalidateTokens(): void { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('invalidateToken') + ->with('token'); + + $this->manager->invalidateToken('token'); + } + + public function testInvalidateTokenById(): void { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('invalidateTokenById') + ->with('uid', 42); + + $this->manager->invalidateTokenById('uid', 42); + } + + public function testInvalidateOldTokens(): void { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('invalidateOldTokens'); + + $this->manager->invalidateOldTokens(); + } + + public function testInvalidateLastUsedBefore(): void { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('invalidateLastUsedBefore') + ->with('user', 946684800); + + $this->manager->invalidateLastUsedBefore('user', 946684800); + } + + public function testGetTokenByUser(): void { + $t1 = new PublicKeyToken(); + $t2 = new PublicKeyToken(); + + $this->publicKeyTokenProvider + ->method('getTokenByUser') + ->willReturn([$t1, $t2]); + + $result = $this->manager->getTokenByUser('uid'); + + $this->assertEquals([$t1, $t2], $result); + } + + public function testRenewSessionTokenPublicKey(): void { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with('oldId', 'newId'); + + $this->manager->renewSessionToken('oldId', 'newId'); + } + + public function testRenewSessionInvalid(): void { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('renewSessionToken') + ->with('oldId', 'newId') + ->willThrowException(new InvalidTokenException()); + + $this->expectException(InvalidTokenException::class); + $this->manager->renewSessionToken('oldId', 'newId'); + } + + public function testGetTokenByIdPublicKey(): void { + $token = $this->createMock(IToken::class); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('getTokenById') + ->with(42) + ->willReturn($token); + + $this->assertSame($token, $this->manager->getTokenById(42)); + } + + public function testGetTokenByIdInvalid(): void { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('getTokenById') + ->with(42) + ->willThrowException(new InvalidTokenException()); + + $this->expectException(InvalidTokenException::class); + $this->manager->getTokenById(42); + } + + public function testGetTokenPublicKey(): void { + $token = new PublicKeyToken(); + + $this->publicKeyTokenProvider + ->method('getToken') + ->with('tokenId') + ->willReturn($token); + + $this->assertSame($token, $this->manager->getToken('tokenId')); + } + + public function testGetTokenInvalid(): void { + $this->publicKeyTokenProvider + ->method('getToken') + ->with('tokenId') + ->willThrowException(new InvalidTokenException()); + + $this->expectException(InvalidTokenException::class); + $this->manager->getToken('tokenId'); + } + + public function testRotateInvalid(): void { + $this->expectException(InvalidTokenException::class); + $this->manager->rotate($this->createMock(IToken::class), 'oldId', 'newId'); + } + + public function testRotatePublicKey(): void { + $token = new PublicKeyToken(); + + $this->publicKeyTokenProvider + ->method('rotate') + ->with($token, 'oldId', 'newId') + ->willReturn($token); + + $this->assertSame($token, $this->manager->rotate($token, 'oldId', 'newId')); + } + + public function testMarkPasswordInvalidPublicKey(): void { + $token = $this->createMock(PublicKeyToken::class); + + $this->publicKeyTokenProvider->expects($this->once()) + ->method('markPasswordInvalid') + ->with($token, 'tokenId'); + + $this->manager->markPasswordInvalid($token, 'tokenId'); + } + + public function testMarkPasswordInvalidInvalidToken(): void { + $this->expectException(InvalidTokenException::class); + + $this->manager->markPasswordInvalid($this->createMock(IToken::class), 'tokenId'); + } + + public function testUpdatePasswords(): void { + $this->publicKeyTokenProvider->expects($this->once()) + ->method('updatePasswords') + ->with('uid', 'pass'); + + $this->manager->updatePasswords('uid', 'pass'); + } + + public function testInvalidateTokensOfUserNoClientName(): void { + $t1 = new PublicKeyToken(); + $t2 = new PublicKeyToken(); + $t1->setId(123); + $t2->setId(456); + + $this->publicKeyTokenProvider + ->expects($this->once()) + ->method('getTokenByUser') + ->with('theUser') + ->willReturn([$t1, $t2]); + + $calls = [ + ['theUser', 123], + ['theUser', 456], + ]; + $this->publicKeyTokenProvider + ->expects($this->exactly(2)) + ->method('invalidateTokenById') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + $this->manager->invalidateTokensOfUser('theUser', null); + } + + public function testInvalidateTokensOfUserClientNameGiven(): void { + $t1 = new PublicKeyToken(); + $t2 = new PublicKeyToken(); + $t3 = new PublicKeyToken(); + $t1->setId(123); + $t1->setName('Firefox session'); + $t2->setId(456); + $t2->setName('My Client Name'); + $t3->setId(789); + $t3->setName('mobile client'); + + $this->publicKeyTokenProvider + ->expects($this->once()) + ->method('getTokenByUser') + ->with('theUser') + ->willReturn([$t1, $t2, $t3]); + $this->publicKeyTokenProvider + ->expects($this->once()) + ->method('invalidateTokenById') + ->with('theUser', 456); + $this->manager->invalidateTokensOfUser('theUser', 'My Client Name'); + } +} diff --git a/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php new file mode 100644 index 00000000000..d1585dadc26 --- /dev/null +++ b/tests/lib/Authentication/Token/PublicKeyTokenMapperTest.php @@ -0,0 +1,265 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Token; + +use OC\Authentication\Token\PublicKeyToken; +use OC\Authentication\Token\PublicKeyTokenMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\Authentication\Token\IToken; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUser; +use OCP\Server; +use Test\TestCase; + +/** + * @group DB + */ +class PublicKeyTokenMapperTest extends TestCase { + /** @var PublicKeyTokenMapper */ + private $mapper; + + /** @var IDBConnection */ + private $dbConnection; + + /** @var int */ + private $time; + + protected function setUp(): void { + parent::setUp(); + + $this->dbConnection = Server::get(IDBConnection::class); + $this->time = time(); + $this->resetDatabase(); + + $this->mapper = new PublicKeyTokenMapper($this->dbConnection); + } + + private function resetDatabase() { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete('authtoken')->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user1'), + 'login_name' => $qb->createNamedParameter('User1'), + 'password' => $qb->createNamedParameter('a75c7116460c082912d8f6860a850904|3nz5qbG1nNSLLi6V|c55365a0e54cfdfac4a175bcf11a7612aea74492277bba6e5d96a24497fa9272488787cb2f3ad34d8b9b8060934fce02f008d371df3ff3848f4aa61944851ff0'), + 'name' => $qb->createNamedParameter('Firefox on Linux'), + 'token' => $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago + 'last_check' => $this->time - 60 * 10, // 10mins ago + 'public_key' => $qb->createNamedParameter('public key'), + 'private_key' => $qb->createNamedParameter('private key'), + 'version' => $qb->createNamedParameter(2), + ])->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user2'), + 'login_name' => $qb->createNamedParameter('User2'), + 'password' => $qb->createNamedParameter('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'), + 'name' => $qb->createNamedParameter('Firefox on Android'), + 'token' => $qb->createNamedParameter('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 60 * 60 * 24 * 3, IQueryBuilder::PARAM_INT), // Three days ago + 'last_check' => $this->time - 10, // 10secs ago + 'public_key' => $qb->createNamedParameter('public key'), + 'private_key' => $qb->createNamedParameter('private key'), + 'version' => $qb->createNamedParameter(2), + ])->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user1'), + 'login_name' => $qb->createNamedParameter('User1'), + 'password' => $qb->createNamedParameter('063de945d6f6b26862d9b6f40652f2d5|DZ/z520tfdXPtd0T|395f6b89be8d9d605e409e20b9d9abe477fde1be38a3223f9e508f979bf906e50d9eaa4dca983ca4fb22a241eb696c3f98654e7775f78c4caf13108f98642b53'), + 'name' => $qb->createNamedParameter('Iceweasel on Linux'), + 'token' => $qb->createNamedParameter('47af8697ba590fb82579b5f1b3b6e8066773a62100abbe0db09a289a62f5d980dc300fa3d98b01d7228468d1ab05c1aa14c8d14bd5b6eee9cdf1ac14864680c3'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago + 'last_check' => $this->time - 60 * 10, // 10mins ago + 'public_key' => $qb->createNamedParameter('public key'), + 'private_key' => $qb->createNamedParameter('private key'), + 'version' => $qb->createNamedParameter(2), + ])->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user3'), + 'login_name' => $qb->createNamedParameter('User3'), + 'password' => $qb->createNamedParameter('063de945d6f6b26862d9b6f40652f2d5|DZ/z520tfdXPtd0T|395f6b89be8d9d605e409e20b9d9abe477fde1be38a3223f9e508f979bf906e50d9eaa4dca983ca4fb22a241eb696c3f98654e7775f78c4caf13108f98642b53'), + 'name' => $qb->createNamedParameter('Iceweasel on Linux'), + 'token' => $qb->createNamedParameter('6d9a290d239d09f2cc33a03cc54cccd46f7dc71630dcc27d39214824bd3e093f1feb4e2b55eb159d204caa15dee9556c202a5aa0b9d67806c3f4ec2cde11af67'), + 'type' => $qb->createNamedParameter(IToken::TEMPORARY_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 120, IQueryBuilder::PARAM_INT), // Two minutes ago + 'last_check' => $this->time - 60 * 10, // 10mins ago + 'public_key' => $qb->createNamedParameter('public key'), + 'private_key' => $qb->createNamedParameter('private key'), + 'version' => $qb->createNamedParameter(2), + 'password_invalid' => $qb->createNamedParameter(1), + ])->execute(); + $qb->insert('authtoken')->values([ + 'uid' => $qb->createNamedParameter('user3'), + 'login_name' => $qb->createNamedParameter('User3'), + 'password' => $qb->createNamedParameter('063de945d6f6b26862d9b6f40652f2d5|DZ/z520tfdXPtd0T|395f6b89be8d9d605e409e20b9d9abe477fde1be38a3223f9e508f979bf906e50d9eaa4dca983ca4fb22a241eb696c3f98654e7775f78c4caf13108f98642b53'), + 'name' => $qb->createNamedParameter('Iceweasel on Linux'), + 'token' => $qb->createNamedParameter('84c5808c6445b6d65b8aa5b03840f09b27de603f0fb970906fb14ea4b115b7bf5ec53fada5c093fe46afdcd7bbc9617253a4d105f7dfb32719f9973d72412f31'), + 'type' => $qb->createNamedParameter(IToken::PERMANENT_TOKEN), + 'last_activity' => $qb->createNamedParameter($this->time - 60 * 3, IQueryBuilder::PARAM_INT), // Three minutes ago + 'last_check' => $this->time - 60 * 10, // 10mins ago + 'public_key' => $qb->createNamedParameter('public key'), + 'private_key' => $qb->createNamedParameter('private key'), + 'version' => $qb->createNamedParameter(2), + 'password_invalid' => $qb->createNamedParameter(1), + ])->execute(); + } + + private function getNumberOfTokens() { + $qb = $this->dbConnection->getQueryBuilder(); + $result = $qb->select($qb->func()->count('*', 'count')) + ->from('authtoken') + ->execute() + ->fetch(); + return (int)$result['count']; + } + + public function testInvalidate(): void { + $token = '9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'; + + $this->mapper->invalidate($token); + + $this->assertSame(4, $this->getNumberOfTokens()); + } + + public function testInvalidateInvalid(): void { + $token = 'youwontfindthisoneinthedatabase'; + + $this->mapper->invalidate($token); + + $this->assertSame(5, $this->getNumberOfTokens()); + } + + public function testInvalidateOld(): void { + $olderThan = $this->time - 60 * 60; // One hour + + $this->mapper->invalidateOld($olderThan); + + $this->assertSame(4, $this->getNumberOfTokens()); + } + + public function testInvalidateLastUsedBefore(): void { + $before = $this->time - 60 * 2; // Two minutes + + $this->mapper->invalidateLastUsedBefore('user3', $before); + + $this->assertSame(4, $this->getNumberOfTokens()); + } + + public function testGetToken(): void { + $token = new PublicKeyToken(); + $token->setUid('user2'); + $token->setLoginName('User2'); + $token->setPassword('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'); + $token->setName('Firefox on Android'); + $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); + $token->setType(IToken::TEMPORARY_TOKEN); + $token->setRemember(IToken::DO_NOT_REMEMBER); + $token->setLastActivity($this->time - 60 * 60 * 24 * 3); + $token->setLastCheck($this->time - 10); + $token->setPublicKey('public key'); + $token->setPrivateKey('private key'); + $token->setVersion(PublicKeyToken::VERSION); + + $dbToken = $this->mapper->getToken($token->getToken()); + + $token->setId($dbToken->getId()); // We don't know the ID + $token->resetUpdatedFields(); + + $this->assertEquals($token, $dbToken); + } + + + public function testGetInvalidToken(): void { + $this->expectException(DoesNotExistException::class); + + $token = 'thisisaninvalidtokenthatisnotinthedatabase'; + + $this->mapper->getToken($token); + } + + public function testGetTokenById(): void { + $token = new PublicKeyToken(); + $token->setUid('user2'); + $token->setLoginName('User2'); + $token->setPassword('971a337057853344700bbeccf836519f|UwOQwyb34sJHtqPV|036d4890f8c21d17bbc7b88072d8ef049a5c832a38e97f3e3d5f9186e896c2593aee16883f617322fa242728d0236ff32d163caeb4bd45e14ca002c57a88665f'); + $token->setName('Firefox on Android'); + $token->setToken('1504445f1524fc801035448a95681a9378ba2e83930c814546c56e5d6ebde221198792fd900c88ed5ead0555780dad1ebce3370d7e154941cd5de87eb419899b'); + $token->setType(IToken::TEMPORARY_TOKEN); + $token->setRemember(IToken::DO_NOT_REMEMBER); + $token->setLastActivity($this->time - 60 * 60 * 24 * 3); + $token->setLastCheck($this->time - 10); + $token->setPublicKey('public key'); + $token->setPrivateKey('private key'); + $token->setVersion(PublicKeyToken::VERSION); + + $dbToken = $this->mapper->getToken($token->getToken()); + $token->setId($dbToken->getId()); // We don't know the ID + $token->resetUpdatedFields(); + + $dbToken = $this->mapper->getTokenById($token->getId()); + $this->assertEquals($token, $dbToken); + } + + + public function testGetTokenByIdNotFound(): void { + $this->expectException(DoesNotExistException::class); + + $this->mapper->getTokenById(-1); + } + + + public function testGetInvalidTokenById(): void { + $this->expectException(DoesNotExistException::class); + + $id = '42'; + + $this->mapper->getToken($id); + } + + public function testGetTokenByUser(): void { + $this->assertCount(2, $this->mapper->getTokenByUser('user1')); + } + + public function testGetTokenByUserNotFound(): void { + $this->assertCount(0, $this->mapper->getTokenByUser('user1000')); + } + + public function testGetById(): void { + /** @var IUser|\PHPUnit\Framework\MockObject\MockObject $user */ + $user = $this->createMock(IUser::class); + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('id') + ->from('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'))); + $result = $qb->execute(); + $id = $result->fetch()['id']; + + $token = $this->mapper->getTokenById((int)$id); + $this->assertEquals('user1', $token->getUID()); + } + + public function testDeleteByName(): void { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('name') + ->from('authtoken') + ->where($qb->expr()->eq('token', $qb->createNamedParameter('9c5a2e661482b65597408a6bb6c4a3d1af36337381872ac56e445a06cdb7fea2b1039db707545c11027a4966919918b19d875a8b774840b18c6cbb7ae56fe206'))); + $result = $qb->execute(); + $name = $result->fetch()['name']; + $this->mapper->deleteByName($name); + $this->assertEquals(4, $this->getNumberOfTokens()); + } + + public function testHasExpiredTokens(): void { + $this->assertFalse($this->mapper->hasExpiredTokens('user1')); + $this->assertTrue($this->mapper->hasExpiredTokens('user3')); + } +} diff --git a/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php new file mode 100644 index 00000000000..7e7f949965f --- /dev/null +++ b/tests/lib/Authentication/Token/PublicKeyTokenProviderTest.php @@ -0,0 +1,645 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Token; + +use OC\Authentication\Exceptions\ExpiredTokenException; +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Exceptions\PasswordlessTokenException; +use OC\Authentication\Token\PublicKeyToken; +use OC\Authentication\Token\PublicKeyTokenMapper; +use OC\Authentication\Token\PublicKeyTokenProvider; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Authentication\Token\IToken; +use OCP\ICacheFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\Security\ICrypto; +use OCP\Security\IHasher; +use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class PublicKeyTokenProviderTest extends TestCase { + /** @var PublicKeyTokenProvider|\PHPUnit\Framework\MockObject\MockObject */ + private $tokenProvider; + /** @var PublicKeyTokenMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $mapper; + /** @var IHasher|\PHPUnit\Framework\MockObject\MockObject */ + private $hasher; + /** @var ICrypto */ + private $crypto; + /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ + private $config; + /** @var IDBConnection|MockObject */ + private IDBConnection $db; + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + /** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $cacheFactory; + /** @var int */ + private $time; + + protected function setUp(): void { + parent::setUp(); + + $this->mapper = $this->createMock(PublicKeyTokenMapper::class); + $this->hasher = Server::get(IHasher::class); + $this->crypto = Server::get(ICrypto::class); + $this->config = $this->createMock(IConfig::class); + $this->config->method('getSystemValue') + ->willReturnMap([ + ['openssl', [], []], + ]); + $this->config->method('getSystemValueString') + ->willReturnMap([ + ['secret', '', '1f4h9s'], + ]); + $this->db = $this->createMock(IDBConnection::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->time = 1313131; + $this->timeFactory->method('getTime') + ->willReturn($this->time); + $this->cacheFactory = $this->createMock(ICacheFactory::class); + + $this->tokenProvider = new PublicKeyTokenProvider( + $this->mapper, + $this->crypto, + $this->config, + $this->db, + $this->logger, + $this->timeFactory, + $this->hasher, + $this->cacheFactory, + ); + } + + public function testGenerateToken(): void { + $token = 'tokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->assertInstanceOf(PublicKeyToken::class, $actual); + $this->assertSame($uid, $actual->getUID()); + $this->assertSame($user, $actual->getLoginName()); + $this->assertSame($name, $actual->getName()); + $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember()); + $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token)); + } + + public function testGenerateTokenNoPassword(): void { + $token = 'tokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, false], + ]); + $this->expectException(PasswordlessTokenException::class); + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->assertInstanceOf(PublicKeyToken::class, $actual); + $this->assertSame($uid, $actual->getUID()); + $this->assertSame($user, $actual->getLoginName()); + $this->assertSame($name, $actual->getName()); + $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember()); + $this->tokenProvider->getPassword($actual, $token); + } + + public function testGenerateTokenLongPassword(): void { + $token = 'tokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = ''; + for ($i = 0; $i < 500; $i++) { + $password .= 'e'; + } + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + $this->expectException(\RuntimeException::class); + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + } + + public function testGenerateTokenInvalidName(): void { + $token = 'tokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12' + . 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->assertInstanceOf(PublicKeyToken::class, $actual); + $this->assertSame($uid, $actual->getUID()); + $this->assertSame($user, $actual->getLoginName()); + $this->assertSame('User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12User-Agent: Mozill…', $actual->getName()); + $this->assertSame(IToken::DO_NOT_REMEMBER, $actual->getRemember()); + $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token)); + } + + public function testUpdateToken(): void { + $tk = new PublicKeyToken(); + $this->mapper->expects($this->once()) + ->method('updateActivity') + ->with($tk, $this->time); + $tk->setLastActivity($this->time - 200); + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + + $this->tokenProvider->updateTokenActivity($tk); + + $this->assertEquals($this->time, $tk->getLastActivity()); + } + + public function testUpdateTokenDebounce(): void { + $tk = new PublicKeyToken(); + $this->config->method('getSystemValueInt') + ->willReturnCallback(function ($value, $default) { + return $default; + }); + $tk->setLastActivity($this->time - 30); + + $this->mapper->expects($this->never()) + ->method('updateActivity') + ->with($tk, $this->time); + + $this->tokenProvider->updateTokenActivity($tk); + } + + public function testGetTokenByUser(): void { + $this->mapper->expects($this->once()) + ->method('getTokenByUser') + ->with('uid') + ->willReturn(['token']); + + $this->assertEquals(['token'], $this->tokenProvider->getTokenByUser('uid')); + } + + public function testGetPassword(): void { + $token = 'tokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->assertSame($password, $this->tokenProvider->getPassword($actual, $token)); + } + + + public function testGetPasswordPasswordLessToken(): void { + $this->expectException(PasswordlessTokenException::class); + + $token = 'token1234'; + $tk = new PublicKeyToken(); + $tk->setPassword(null); + + $this->tokenProvider->getPassword($tk, $token); + } + + + public function testGetPasswordInvalidToken(): void { + $this->expectException(InvalidTokenException::class); + + $token = 'tokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->tokenProvider->getPassword($actual, 'wrongtoken'); + } + + public function testSetPassword(): void { + $token = 'tokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->mapper->method('getTokenByUser') + ->with('user') + ->willReturn([$actual]); + + $newpass = 'newpass'; + $this->mapper->expects($this->once()) + ->method('update') + ->with($this->callback(function ($token) use ($newpass) { + return $newpass === $this->tokenProvider->getPassword($token, 'tokentokentokentokentoken'); + })); + + + $this->tokenProvider->setPassword($actual, $token, $newpass); + + $this->assertSame($newpass, $this->tokenProvider->getPassword($actual, 'tokentokentokentokentoken')); + } + + + public function testSetPasswordInvalidToken(): void { + $this->expectException(InvalidTokenException::class); + + $token = $this->createMock(IToken::class); + $tokenId = 'token123'; + $password = '123456'; + + $this->tokenProvider->setPassword($token, $tokenId, $password); + } + + public function testInvalidateToken(): void { + $calls = [ + [hash('sha512', 'token7' . '1f4h9s')], + [hash('sha512', 'token7')] + ]; + + $this->mapper->expects($this->exactly(2)) + ->method('invalidate') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->tokenProvider->invalidateToken('token7'); + } + + public function testInvalidateTokenById(): void { + $id = 123; + + $this->mapper->expects($this->once()) + ->method('getTokenById') + ->with($id); + + $this->tokenProvider->invalidateTokenById('uid', $id); + } + + public function testInvalidateOldTokens(): void { + $defaultSessionLifetime = 60 * 60 * 24; + $defaultRememberMeLifetime = 60 * 60 * 24 * 15; + $wipeTokenLifetime = 60 * 60 * 24 * 60; + $this->config->expects($this->exactly(4)) + ->method('getSystemValueInt') + ->willReturnMap([ + ['session_lifetime', $defaultSessionLifetime, 150], + ['remember_login_cookie_lifetime', $defaultRememberMeLifetime, 300], + ['token_auth_wipe_token_retention', $wipeTokenLifetime, 500], + ['token_auth_token_retention', 60 * 60 * 24 * 365, 800], + ]); + + $calls = [ + [$this->time - 150, IToken::TEMPORARY_TOKEN, IToken::DO_NOT_REMEMBER], + [$this->time - 300, IToken::TEMPORARY_TOKEN, IToken::REMEMBER], + [$this->time - 500, IToken::WIPE_TOKEN, null], + [$this->time - 800, IToken::PERMANENT_TOKEN, null], + ]; + $this->mapper->expects($this->exactly(4)) + ->method('invalidateOld') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->tokenProvider->invalidateOldTokens(); + } + + public function testInvalidateLastUsedBefore(): void { + $this->mapper->expects($this->once()) + ->method('invalidateLastUsedBefore') + ->with('user', 946684800); + + $this->tokenProvider->invalidateLastUsedBefore('user', 946684800); + } + + public function testRenewSessionTokenWithoutPassword(): void { + $token = 'oldIdtokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = null; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->mapper + ->expects($this->once()) + ->method('getToken') + ->with(hash('sha512', 'oldIdtokentokentokentoken' . '1f4h9s')) + ->willReturn($oldToken); + $this->mapper + ->expects($this->once()) + ->method('insert') + ->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name) { + return $token->getUID() === $uid + && $token->getLoginName() === $user + && $token->getName() === $name + && $token->getType() === IToken::DO_NOT_REMEMBER + && $token->getLastActivity() === $this->time + && $token->getPassword() === null; + })); + $this->mapper + ->expects($this->once()) + ->method('delete') + ->with($this->callback(function ($token) use ($oldToken) { + return $token === $oldToken; + })); + + $this->tokenProvider->renewSessionToken('oldIdtokentokentokentoken', 'newIdtokentokentokentoken'); + } + + public function testRenewSessionTokenWithPassword(): void { + $token = 'oldIdtokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'password'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + $oldToken = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $this->mapper + ->expects($this->once()) + ->method('getToken') + ->with(hash('sha512', 'oldIdtokentokentokentoken' . '1f4h9s')) + ->willReturn($oldToken); + $this->mapper + ->expects($this->once()) + ->method('insert') + ->with($this->callback(function (PublicKeyToken $token) use ($user, $uid, $name): bool { + return $token->getUID() === $uid + && $token->getLoginName() === $user + && $token->getName() === $name + && $token->getType() === IToken::DO_NOT_REMEMBER + && $token->getLastActivity() === $this->time + && $token->getPassword() !== null + && $this->tokenProvider->getPassword($token, 'newIdtokentokentokentoken') === 'password'; + })); + $this->mapper + ->expects($this->once()) + ->method('delete') + ->with($this->callback(function ($token) use ($oldToken): bool { + return $token === $oldToken; + })); + + $this->tokenProvider->renewSessionToken('oldIdtokentokentokentoken', 'newIdtokentokentokentoken'); + } + + public function testGetToken(): void { + $token = new PublicKeyToken(); + + $this->config->method('getSystemValue') + ->with('secret') + ->willReturn('mysecret'); + + $this->mapper->method('getToken') + ->with( + $this->callback(function (string $token) { + return hash('sha512', 'unhashedTokentokentokentokentoken' . '1f4h9s') === $token; + }) + )->willReturn($token); + + $this->assertSame($token, $this->tokenProvider->getToken('unhashedTokentokentokentokentoken')); + } + + public function testGetInvalidToken(): void { + $this->expectException(InvalidTokenException::class); + + $calls = [ + 'unhashedTokentokentokentokentoken' . '1f4h9s', + 'unhashedTokentokentokentokentoken', + ]; + $this->mapper->expects($this->exactly(2)) + ->method('getToken') + ->willReturnCallback(function (string $token) use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals(hash('sha512', $expected), $token); + throw new DoesNotExistException('nope'); + }); + + $this->tokenProvider->getToken('unhashedTokentokentokentokentoken'); + } + + public function testGetExpiredToken(): void { + $token = 'tokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'passme'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + $actual->setExpires(42); + + $this->mapper->method('getToken') + ->with( + $this->callback(function (string $token) { + return hash('sha512', 'tokentokentokentokentoken' . '1f4h9s') === $token; + }) + )->willReturn($actual); + + try { + $this->tokenProvider->getToken('tokentokentokentokentoken'); + $this->fail(); + } catch (ExpiredTokenException $e) { + $this->assertSame($actual, $e->getToken()); + } + } + + public function testGetTokenById(): void { + $token = $this->createMock(PublicKeyToken::class); + + $this->mapper->expects($this->once()) + ->method('getTokenById') + ->with($this->equalTo(42)) + ->willReturn($token); + + $this->assertSame($token, $this->tokenProvider->getTokenById(42)); + } + + public function testGetInvalidTokenById(): void { + $this->expectException(InvalidTokenException::class); + + $this->mapper->expects($this->once()) + ->method('getTokenById') + ->with($this->equalTo(42)) + ->willThrowException(new DoesNotExistException('nope')); + + $this->tokenProvider->getTokenById(42); + } + + public function testGetExpiredTokenById(): void { + $token = new PublicKeyToken(); + $token->setExpires(42); + + $this->mapper->expects($this->once()) + ->method('getTokenById') + ->with($this->equalTo(42)) + ->willReturn($token); + + try { + $this->tokenProvider->getTokenById(42); + $this->fail(); + } catch (ExpiredTokenException $e) { + $this->assertSame($token, $e->getToken()); + } + } + + public function testRotate(): void { + $token = 'oldtokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = 'password'; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $new = $this->tokenProvider->rotate($actual, 'oldtokentokentokentokentoken', 'newtokentokentokentokentoken'); + + $this->assertSame('password', $this->tokenProvider->getPassword($new, 'newtokentokentokentokentoken')); + } + + public function testRotateNoPassword(): void { + $token = 'oldtokentokentokentokentoken'; + $uid = 'user'; + $user = 'User'; + $password = null; + $name = 'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.12) Gecko/20101026 Firefox/3.6.12'; + $type = IToken::PERMANENT_TOKEN; + + $actual = $this->tokenProvider->generateToken($token, $uid, $user, $password, $name, $type, IToken::DO_NOT_REMEMBER); + + $oldPrivate = $actual->getPrivateKey(); + + $new = $this->tokenProvider->rotate($actual, 'oldtokentokentokentokentoken', 'newtokentokentokentokentoken'); + + $newPrivate = $new->getPrivateKey(); + + $this->assertNotSame($newPrivate, $oldPrivate); + $this->assertNull($new->getPassword()); + } + + public function testMarkPasswordInvalidInvalidToken(): void { + $token = $this->createMock(IToken::class); + + $this->expectException(InvalidTokenException::class); + + $this->tokenProvider->markPasswordInvalid($token, 'tokenId'); + } + + public function testMarkPasswordInvalid(): void { + $token = $this->createMock(PublicKeyToken::class); + + $token->expects($this->once()) + ->method('setPasswordInvalid') + ->with(true); + $this->mapper->expects($this->once()) + ->method('update') + ->with($token); + + $this->tokenProvider->markPasswordInvalid($token, 'tokenId'); + } + + public function testUpdatePasswords(): void { + $uid = 'myUID'; + $token1 = $this->tokenProvider->generateToken( + 'foobetokentokentokentoken', + $uid, + $uid, + 'bar', + 'random1', + IToken::PERMANENT_TOKEN, + IToken::REMEMBER); + $token2 = $this->tokenProvider->generateToken( + 'foobartokentokentokentoken', + $uid, + $uid, + 'bar', + 'random2', + IToken::PERMANENT_TOKEN, + IToken::REMEMBER); + $this->config->method('getSystemValueBool') + ->willReturnMap([ + ['auth.storeCryptedPassword', true, true], + ]); + + $this->mapper->method('hasExpiredTokens') + ->with($uid) + ->willReturn(true); + $this->mapper->expects($this->once()) + ->method('getTokenByUser') + ->with($uid) + ->willReturn([$token1, $token2]); + $this->mapper->expects($this->exactly(2)) + ->method('update') + ->with($this->callback(function (PublicKeyToken $t) use ($token1, $token2) { + return $t === $token1 || $t === $token2; + })); + + $this->tokenProvider->updatePasswords($uid, 'bar2'); + } +} diff --git a/tests/lib/Authentication/Token/PublicKeyTokenTest.php b/tests/lib/Authentication/Token/PublicKeyTokenTest.php new file mode 100644 index 00000000000..5f5f29c865f --- /dev/null +++ b/tests/lib/Authentication/Token/PublicKeyTokenTest.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Token; + +use OC\Authentication\Token\PublicKeyToken; +use OCP\Authentication\Token\IToken; +use Test\TestCase; + +class PublicKeyTokenTest extends TestCase { + public function testSetScopeAsArray(): void { + $scope = [IToken::SCOPE_FILESYSTEM => false]; + $token = new PublicKeyToken(); + $token->setScope($scope); + $this->assertEquals(json_encode($scope), $token->getScope()); + $this->assertEquals($scope, $token->getScopeAsArray()); + } + + public function testDefaultScope(): void { + $scope = [IToken::SCOPE_FILESYSTEM => true]; + $token = new PublicKeyToken(); + $this->assertEquals($scope, $token->getScopeAsArray()); + } +} diff --git a/tests/lib/Authentication/Token/RemoteWipeTest.php b/tests/lib/Authentication/Token/RemoteWipeTest.php new file mode 100644 index 00000000000..ca09767c759 --- /dev/null +++ b/tests/lib/Authentication/Token/RemoteWipeTest.php @@ -0,0 +1,173 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\Token; + +use OC\Authentication\Events\RemoteWipeFinished; +use OC\Authentication\Events\RemoteWipeStarted; +use OC\Authentication\Exceptions\WipeTokenException; +use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IProvider as ITokenProvider; +use OC\Authentication\Token\IToken; +use OC\Authentication\Token\IWipeableToken; +use OC\Authentication\Token\RemoteWipe; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class RemoteWipeTest extends TestCase { + /** @var ITokenProvider|MockObject */ + private $tokenProvider; + + /** @var IEventDispatcher|MockObject */ + private $eventDispatcher; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var RemoteWipe */ + private $remoteWipe; + + protected function setUp(): void { + parent::setUp(); + + $this->tokenProvider = $this->createMock(IProvider::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->remoteWipe = new RemoteWipe( + $this->tokenProvider, + $this->eventDispatcher, + $this->logger + ); + } + + public function testMarkNonWipableTokenForWipe(): void { + $token = $this->createMock(IToken::class); + $result = $this->remoteWipe->markTokenForWipe($token); + $this->assertFalse($result); + } + + public function testMarkTokenForWipe(): void { + $token = $this->createMock(IWipeableToken::class); + $token->expects($this->once()) + ->method('wipe'); + + $this->tokenProvider->expects($this->once()) + ->method('updateToken') + ->with($token); + + $result = $this->remoteWipe->markTokenForWipe($token); + $this->assertTrue($result); + } + + public function testMarkAllTokensForWipeNoWipeableToken(): void { + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $token1 = $this->createMock(IToken::class); + $token2 = $this->createMock(IToken::class); + $this->tokenProvider->expects($this->once()) + ->method('getTokenByUser') + ->with('user123') + ->willReturn([$token1, $token2]); + + $result = $this->remoteWipe->markAllTokensForWipe($user); + + $this->assertFalse($result); + } + + public function testMarkAllTokensForWipe(): void { + /** @var IUser|MockObject $user */ + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $token1 = $this->createMock(IToken::class); + $token2 = $this->createMock(IWipeableToken::class); + $this->tokenProvider->expects($this->once()) + ->method('getTokenByUser') + ->with('user123') + ->willReturn([$token1, $token2]); + $token2->expects($this->once()) + ->method('wipe'); + $this->tokenProvider->expects($this->once()) + ->method('updateToken') + ->with($token2); + + $result = $this->remoteWipe->markAllTokensForWipe($user); + + $this->assertTrue($result); + } + + public function testStartWipingNotAWipeToken(): void { + $token = $this->createMock(IToken::class); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('tk1') + ->willReturn($token); + $this->eventDispatcher->expects($this->never()) + ->method('dispatch'); + + $result = $this->remoteWipe->start('tk1'); + + $this->assertFalse($result); + } + + public function testStartWiping(): void { + $token = $this->createMock(IToken::class); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('tk1') + ->willThrowException(new WipeTokenException($token)); + $this->eventDispatcher->expects($this->once()) + ->method('dispatch'); + $this->eventDispatcher->expects($this->once()) + ->method('dispatch') + ->with(RemoteWipeStarted::class, $this->equalTo(new RemoteWipeStarted($token))); + + $result = $this->remoteWipe->start('tk1'); + + $this->assertTrue($result); + } + + public function testFinishWipingNotAWipeToken(): void { + $token = $this->createMock(IToken::class); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('tk1') + ->willReturn($token); + $this->eventDispatcher->expects($this->never()) + ->method('dispatch'); + + $result = $this->remoteWipe->finish('tk1'); + + $this->assertFalse($result); + } + + public function startFinishWiping() { + $token = $this->createMock(IToken::class); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('tk1') + ->willThrowException(new WipeTokenException($token)); + $this->eventDispatcher->expects($this->once()) + ->method('dispatch'); + $this->tokenProvider->expects($this->once()) + ->method('invalidateToken') + ->with($token); + $this->eventDispatcher->expects($this->once()) + ->method('dispatch') + ->with(RemoteWipeFinished::class, $this->equalTo(new RemoteWipeFinished($token))); + + $result = $this->remoteWipe->finish('tk1'); + + $this->assertTrue($result); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php b/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php new file mode 100644 index 00000000000..b59ef876ffd --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDaoTest.php @@ -0,0 +1,159 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\TwoFactorAuth\Db; + +use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao; +use OCP\IDBConnection; +use OCP\Server; +use Test\TestCase; + +/** + * @group DB + */ +class ProviderUserAssignmentDaoTest extends TestCase { + /** @var IDBConnection */ + private $dbConn; + + /** @var ProviderUserAssignmentDao */ + private $dao; + + protected function setUp(): void { + parent::setUp(); + + $this->dbConn = Server::get(IDBConnection::class); + $qb = $this->dbConn->getQueryBuilder(); + $q = $qb->delete(ProviderUserAssignmentDao::TABLE_NAME); + $q->execute(); + + $this->dao = new ProviderUserAssignmentDao($this->dbConn); + } + + public function testGetState(): void { + $qb = $this->dbConn->getQueryBuilder(); + $q1 = $qb->insert(ProviderUserAssignmentDao::TABLE_NAME)->values([ + 'provider_id' => $qb->createNamedParameter('twofactor_u2f'), + 'uid' => $qb->createNamedParameter('user123'), + 'enabled' => $qb->createNamedParameter(1), + ]); + $q1->execute(); + $q2 = $qb->insert(ProviderUserAssignmentDao::TABLE_NAME)->values([ + 'provider_id' => $qb->createNamedParameter('twofactor_totp'), + 'uid' => $qb->createNamedParameter('user123'), + 'enabled' => $qb->createNamedParameter(0), + ]); + $q2->execute(); + $expected = [ + 'twofactor_u2f' => true, + 'twofactor_totp' => false, + ]; + + $state = $this->dao->getState('user123'); + + $this->assertEquals($expected, $state); + } + + public function testPersist(): void { + $qb = $this->dbConn->getQueryBuilder(); + + $this->dao->persist('twofactor_totp', 'user123', 0); + + $q = $qb + ->select('*') + ->from(ProviderUserAssignmentDao::TABLE_NAME) + ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter('twofactor_totp'))) + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter('user123'))) + ->andWhere($qb->expr()->eq('enabled', $qb->createNamedParameter(0))); + $res = $q->execute(); + $data = $res->fetchAll(); + $res->closeCursor(); + $this->assertCount(1, $data); + } + + public function testPersistTwice(): void { + $qb = $this->dbConn->getQueryBuilder(); + + $this->dao->persist('twofactor_totp', 'user123', 0); + $this->dao->persist('twofactor_totp', 'user123', 1); + + $q = $qb + ->select('*') + ->from(ProviderUserAssignmentDao::TABLE_NAME) + ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter('twofactor_totp'))) + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter('user123'))) + ->andWhere($qb->expr()->eq('enabled', $qb->createNamedParameter(1))); + $res = $q->execute(); + $data = $res->fetchAll(); + $res->closeCursor(); + + $this->assertCount(1, $data); + } + + public function testPersistSameStateTwice(): void { + $qb = $this->dbConn->getQueryBuilder(); + + $this->dao->persist('twofactor_totp', 'user123', 1); + $this->dao->persist('twofactor_totp', 'user123', 1); + + $q = $qb + ->select('*') + ->from(ProviderUserAssignmentDao::TABLE_NAME) + ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter('twofactor_totp'))) + ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter('user123'))) + ->andWhere($qb->expr()->eq('enabled', $qb->createNamedParameter(1))); + $res = $q->execute(); + $data = $res->fetchAll(); + $res->closeCursor(); + + $this->assertCount(1, $data); + } + + public function testDeleteByUser(): void { + $this->dao->persist('twofactor_fail', 'user1', 1); + $this->dao->persist('twofactor_u2f', 'user1', 1); + $this->dao->persist('twofactor_fail', 'user2', 0); + $this->dao->persist('twofactor_u2f', 'user2', 0); + + $deleted = $this->dao->deleteByUser('user1'); + + $this->assertEquals( + [ + [ + 'uid' => 'user1', + 'provider_id' => 'twofactor_fail', + 'enabled' => true, + ], + [ + 'uid' => 'user1', + 'provider_id' => 'twofactor_u2f', + 'enabled' => true, + ], + ], + $deleted + ); + $statesUser1 = $this->dao->getState('user1'); + $statesUser2 = $this->dao->getState('user2'); + $this->assertCount(0, $statesUser1); + $this->assertCount(2, $statesUser2); + } + + public function testDeleteAll(): void { + $this->dao->persist('twofactor_fail', 'user1', 1); + $this->dao->persist('twofactor_u2f', 'user1', 1); + $this->dao->persist('twofactor_fail', 'user2', 0); + $this->dao->persist('twofactor_u2f', 'user1', 0); + + $this->dao->deleteAll('twofactor_fail'); + + $statesUser1 = $this->dao->getState('user1'); + $statesUser2 = $this->dao->getState('user2'); + $this->assertCount(1, $statesUser1); + $this->assertCount(0, $statesUser2); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/EnforcementStateTest.php b/tests/lib/Authentication/TwoFactorAuth/EnforcementStateTest.php new file mode 100644 index 00000000000..f1d38c10801 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/EnforcementStateTest.php @@ -0,0 +1,51 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +/** + * Created by PhpStorm. + * User: christoph + * Date: 11.10.18 + * Time: 13:01 + */ + +namespace Tests\Authentication\TwoFactorAuth; + +use OC\Authentication\TwoFactorAuth\EnforcementState; +use Test\TestCase; + +class EnforcementStateTest extends TestCase { + public function testIsEnforced(): void { + $state = new EnforcementState(true); + + $this->assertTrue($state->isEnforced()); + } + + public function testGetEnforcedGroups(): void { + $state = new EnforcementState(true, ['twofactorers']); + + $this->assertEquals(['twofactorers'], $state->getEnforcedGroups()); + } + + public function testGetExcludedGroups(): void { + $state = new EnforcementState(true, [], ['yoloers']); + + $this->assertEquals(['yoloers'], $state->getExcludedGroups()); + } + + public function testJsonSerialize(): void { + $state = new EnforcementState(true, ['twofactorers'], ['yoloers']); + $expected = [ + 'enforced' => true, + 'enforcedGroups' => ['twofactorers'], + 'excludedGroups' => ['yoloers'], + ]; + + $json = $state->jsonSerialize(); + + $this->assertEquals($expected, $json); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php new file mode 100644 index 00000000000..a2bed8a3652 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/ManagerTest.php @@ -0,0 +1,795 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +namespace Test\Authentication\TwoFactorAuth; + +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\IProvider as TokenProvider; +use OC\Authentication\Token\IToken; +use OC\Authentication\TwoFactorAuth\Manager; +use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; +use OC\Authentication\TwoFactorAuth\ProviderLoader; +use OCP\Activity\IEvent; +use OCP\Activity\IManager; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\Authentication\TwoFactorAuth\IRegistry; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IConfig; +use OCP\ISession; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; +use function reset; + +class ManagerTest extends TestCase { + /** @var IUser|MockObject */ + private $user; + + /** @var ProviderLoader|MockObject */ + private $providerLoader; + + /** @var IRegistry|MockObject */ + private $providerRegistry; + + /** @var MandatoryTwoFactor|MockObject */ + private $mandatoryTwoFactor; + + /** @var ISession|MockObject */ + private $session; + + /** @var Manager */ + private $manager; + + /** @var IConfig|MockObject */ + private $config; + + /** @var IManager|MockObject */ + private $activityManager; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var IProvider|MockObject */ + private $fakeProvider; + + /** @var IProvider|MockObject */ + private $backupProvider; + + /** @var TokenProvider|MockObject */ + private $tokenProvider; + + /** @var ITimeFactory|MockObject */ + private $timeFactory; + + /** @var IEventDispatcher|MockObject */ + private $dispatcher; + + protected function setUp(): void { + parent::setUp(); + + $this->user = $this->createMock(IUser::class); + $this->providerLoader = $this->createMock(ProviderLoader::class); + $this->providerRegistry = $this->createMock(IRegistry::class); + $this->mandatoryTwoFactor = $this->createMock(MandatoryTwoFactor::class); + $this->session = $this->createMock(ISession::class); + $this->config = $this->createMock(IConfig::class); + $this->activityManager = $this->createMock(IManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->tokenProvider = $this->createMock(TokenProvider::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->dispatcher = $this->createMock(IEventDispatcher::class); + + $this->manager = new Manager( + $this->providerLoader, + $this->providerRegistry, + $this->mandatoryTwoFactor, + $this->session, + $this->config, + $this->activityManager, + $this->logger, + $this->tokenProvider, + $this->timeFactory, + $this->dispatcher, + ); + + $this->fakeProvider = $this->createMock(IProvider::class); + $this->fakeProvider->method('getId')->willReturn('email'); + + $this->backupProvider = $this->getMockBuilder('\OCP\Authentication\TwoFactorAuth\IProvider')->getMock(); + $this->backupProvider->method('getId')->willReturn('backup_codes'); + $this->backupProvider->method('isTwoFactorAuthEnabledForUser')->willReturn(true); + } + + private function prepareNoProviders() { + $this->providerLoader->method('getProviders') + ->with($this->user) + ->willReturn([]); + } + + private function prepareProviders() { + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([ + $this->fakeProvider->getId() => true, + ]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([$this->fakeProvider]); + } + + private function prepareProvidersWitBackupProvider() { + $this->providerLoader->method('getProviders') + ->with($this->user) + ->willReturn([ + $this->fakeProvider, + $this->backupProvider, + ]); + } + + public function testIsTwoFactorAuthenticatedEnforced(): void { + $this->mandatoryTwoFactor->expects($this->once()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(true); + + $enabled = $this->manager->isTwoFactorAuthenticated($this->user); + + $this->assertTrue($enabled); + } + + public function testIsTwoFactorAuthenticatedNoProviders(): void { + $this->mandatoryTwoFactor->expects($this->once()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->willReturn([]); // No providers registered + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->willReturn([]); // No providers loadable + + $this->assertFalse($this->manager->isTwoFactorAuthenticated($this->user)); + } + + public function testIsTwoFactorAuthenticatedOnlyBackupCodes(): void { + $this->mandatoryTwoFactor->expects($this->once()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->willReturn([ + 'backup_codes' => true, + ]); + $backupCodesProvider = $this->createMock(IProvider::class); + $backupCodesProvider + ->method('getId') + ->willReturn('backup_codes'); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->willReturn([ + $backupCodesProvider, + ]); + + $this->assertFalse($this->manager->isTwoFactorAuthenticated($this->user)); + } + + public function testIsTwoFactorAuthenticatedFailingProviders(): void { + $this->mandatoryTwoFactor->expects($this->once()) + ->method('isEnforcedFor') + ->with($this->user) + ->willReturn(false); + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->willReturn([ + 'twofactor_totp' => true, + 'twofactor_u2f' => false, + ]); // Two providers registered, but … + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->willReturn([]); // … none of them is able to load, however … + + // … 2FA is still enforced + $this->assertTrue($this->manager->isTwoFactorAuthenticated($this->user)); + } + + public static function providerStatesFixData(): array { + return [ + [false, false], + [true, true], + ]; + } + + /** + * If the 2FA registry has not been populated when a user logs in, + * the 2FA manager has to first fix the state before it checks for + * enabled providers. + * + * If any of these providers is active, 2FA is enabled + */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerStatesFixData')] + public function testIsTwoFactorAuthenticatedFixesProviderStates(bool $providerEnabled, bool $expected): void { + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->willReturn([]); // Nothing registered yet + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->willReturn([ + $this->fakeProvider + ]); + $this->fakeProvider->expects($this->once()) + ->method('isTwoFactorAuthEnabledForUser') + ->with($this->user) + ->willReturn($providerEnabled); + if ($providerEnabled) { + $this->providerRegistry->expects($this->once()) + ->method('enableProviderFor') + ->with( + $this->fakeProvider, + $this->user + ); + } else { + $this->providerRegistry->expects($this->once()) + ->method('disableProviderFor') + ->with( + $this->fakeProvider, + $this->user + ); + } + + $this->assertEquals($expected, $this->manager->isTwoFactorAuthenticated($this->user)); + } + + public function testGetProvider(): void { + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([ + $this->fakeProvider->getId() => true, + ]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([$this->fakeProvider]); + + $provider = $this->manager->getProvider($this->user, $this->fakeProvider->getId()); + + $this->assertSame($this->fakeProvider, $provider); + } + + public function testGetInvalidProvider(): void { + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([]); + + $provider = $this->manager->getProvider($this->user, 'nonexistent'); + + $this->assertNull($provider); + } + + public function testGetLoginSetupProviders(): void { + $provider1 = $this->createMock(IProvider::class); + $provider2 = $this->createMock(IActivatableAtLogin::class); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([ + $provider1, + $provider2, + ]); + + $providers = $this->manager->getLoginSetupProviders($this->user); + + $this->assertCount(1, $providers); + $this->assertSame($provider2, reset($providers)); + } + + public function testGetProviders(): void { + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([ + $this->fakeProvider->getId() => true, + ]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([$this->fakeProvider]); + $expectedProviders = [ + 'email' => $this->fakeProvider, + ]; + + $providerSet = $this->manager->getProviderSet($this->user); + $providers = $providerSet->getProviders(); + + $this->assertEquals($expectedProviders, $providers); + $this->assertFalse($providerSet->isProviderMissing()); + } + + public function testGetProvidersOneMissing(): void { + $this->providerRegistry->expects($this->once()) + ->method('getProviderStates') + ->with($this->user) + ->willReturn([ + $this->fakeProvider->getId() => true, + ]); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($this->user) + ->willReturn([]); + $expectedProviders = [ + 'email' => $this->fakeProvider, + ]; + + $providerSet = $this->manager->getProviderSet($this->user); + + $this->assertTrue($providerSet->isProviderMissing()); + } + + public function testVerifyChallenge(): void { + $this->prepareProviders(); + + $challenge = 'passme'; + $event = $this->createMock(IEvent::class); + $this->fakeProvider->expects($this->once()) + ->method('verifyChallenge') + ->with($this->user, $challenge) + ->willReturn(true); + $this->session->expects($this->once()) + ->method('get') + ->with('two_factor_remember_login') + ->willReturn(false); + + $calls = [ + ['two_factor_auth_uid'], + ['two_factor_remember_login'], + ]; + $this->session->expects($this->exactly(2)) + ->method('remove') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->session->expects($this->once()) + ->method('set') + ->with(Manager::SESSION_UID_DONE, 'jos'); + $this->session->method('getId') + ->willReturn('mysessionid'); + $this->activityManager->expects($this->once()) + ->method('generateEvent') + ->willReturn($event); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('jos'); + $event->expects($this->once()) + ->method('setApp') + ->with($this->equalTo('core')) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setType') + ->with($this->equalTo('security')) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setAuthor') + ->with($this->equalTo('jos')) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setAffectedUser') + ->with($this->equalTo('jos')) + ->willReturnSelf(); + $this->fakeProvider + ->method('getDisplayName') + ->willReturn('Fake 2FA'); + $event->expects($this->once()) + ->method('setSubject') + ->with($this->equalTo('twofactor_success'), $this->equalTo([ + 'provider' => 'Fake 2FA', + ])) + ->willReturnSelf(); + $token = $this->createMock(IToken::class); + $this->tokenProvider->method('getToken') + ->with('mysessionid') + ->willReturn($token); + $token->method('getId') + ->willReturn(42); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with('jos', 'login_token_2fa', '42'); + + $result = $this->manager->verifyChallenge('email', $this->user, $challenge); + + $this->assertTrue($result); + } + + public function testVerifyChallengeInvalidProviderId(): void { + $this->prepareProviders(); + + $challenge = 'passme'; + $this->fakeProvider->expects($this->never()) + ->method('verifyChallenge') + ->with($this->user, $challenge); + $this->session->expects($this->never()) + ->method('remove'); + + $this->assertFalse($this->manager->verifyChallenge('dontexist', $this->user, $challenge)); + } + + public function testVerifyInvalidChallenge(): void { + $this->prepareProviders(); + + $challenge = 'dontpassme'; + $event = $this->createMock(IEvent::class); + $this->fakeProvider->expects($this->once()) + ->method('verifyChallenge') + ->with($this->user, $challenge) + ->willReturn(false); + $this->session->expects($this->never()) + ->method('remove'); + $this->activityManager->expects($this->once()) + ->method('generateEvent') + ->willReturn($event); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn('jos'); + $event->expects($this->once()) + ->method('setApp') + ->with($this->equalTo('core')) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setType') + ->with($this->equalTo('security')) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setAuthor') + ->with($this->equalTo('jos')) + ->willReturnSelf(); + $event->expects($this->once()) + ->method('setAffectedUser') + ->with($this->equalTo('jos')) + ->willReturnSelf(); + $this->fakeProvider + ->method('getDisplayName') + ->willReturn('Fake 2FA'); + $event->expects($this->once()) + ->method('setSubject') + ->with($this->equalTo('twofactor_failed'), $this->equalTo([ + 'provider' => 'Fake 2FA', + ])) + ->willReturnSelf(); + + $this->assertFalse($this->manager->verifyChallenge('email', $this->user, $challenge)); + } + + public function testNeedsSecondFactor(): void { + $user = $this->createMock(IUser::class); + + $calls = [ + ['app_password'], + ['two_factor_auth_uid'], + [Manager::SESSION_UID_DONE], + ]; + $this->session->expects($this->exactly(3)) + ->method('exists') + ->willReturnCallback(function () use (&$calls) { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + return false; + }); + + $this->session->method('getId') + ->willReturn('mysessionid'); + $token = $this->createMock(IToken::class); + $this->tokenProvider->method('getToken') + ->with('mysessionid') + ->willReturn($token); + $token->method('getId') + ->willReturn(42); + + $user->method('getUID') + ->willReturn('user'); + $this->config->method('getUserKeys') + ->with('user', 'login_token_2fa') + ->willReturn([ + '42' + ]); + + $manager = $this->getMockBuilder(Manager::class) + ->setConstructorArgs([ + $this->providerLoader, + $this->providerRegistry, + $this->mandatoryTwoFactor, + $this->session, + $this->config, + $this->activityManager, + $this->logger, + $this->tokenProvider, + $this->timeFactory, + $this->dispatcher, + ]) + ->onlyMethods(['isTwoFactorAuthenticated'])// Do not actually load the apps + ->getMock(); + + $manager->method('isTwoFactorAuthenticated') + ->with($user) + ->willReturn(true); + + $this->assertTrue($manager->needsSecondFactor($user)); + } + + public function testNeedsSecondFactorUserIsNull(): void { + $user = null; + $this->session->expects($this->never()) + ->method('exists'); + + $this->assertFalse($this->manager->needsSecondFactor($user)); + } + + public function testNeedsSecondFactorWithNoProviderAvailableAnymore(): void { + $this->prepareNoProviders(); + + $user = null; + $this->session->expects($this->never()) + ->method('exists') + ->with('two_factor_auth_uid') + ->willReturn(true); + $this->session->expects($this->never()) + ->method('remove') + ->with('two_factor_auth_uid'); + + $this->assertFalse($this->manager->needsSecondFactor($user)); + } + + public function testPrepareTwoFactorLogin(): void { + $this->user->method('getUID') + ->willReturn('ferdinand'); + + $calls = [ + ['two_factor_auth_uid', 'ferdinand'], + ['two_factor_remember_login', true], + ]; + $this->session->expects($this->exactly(2)) + ->method('set') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->session->method('getId') + ->willReturn('mysessionid'); + $token = $this->createMock(IToken::class); + $this->tokenProvider->method('getToken') + ->with('mysessionid') + ->willReturn($token); + $token->method('getId') + ->willReturn(42); + + $this->timeFactory->method('getTime') + ->willReturn(1337); + + $this->config->method('setUserValue') + ->with('ferdinand', 'login_token_2fa', '42', '1337'); + + + $this->manager->prepareTwoFactorLogin($this->user, true); + } + + public function testPrepareTwoFactorLoginDontRemember(): void { + $this->user->method('getUID') + ->willReturn('ferdinand'); + + $calls = [ + ['two_factor_auth_uid', 'ferdinand'], + ['two_factor_remember_login', false], + ]; + $this->session->expects($this->exactly(2)) + ->method('set') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->session->method('getId') + ->willReturn('mysessionid'); + $token = $this->createMock(IToken::class); + $this->tokenProvider->method('getToken') + ->with('mysessionid') + ->willReturn($token); + $token->method('getId') + ->willReturn(42); + + $this->timeFactory->method('getTime') + ->willReturn(1337); + + $this->config->method('setUserValue') + ->with('ferdinand', 'login_token_2fa', '42', '1337'); + + $this->manager->prepareTwoFactorLogin($this->user, false); + } + + public function testNeedsSecondFactorSessionAuth(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->session->method('exists') + ->willReturnCallback(function ($var) { + if ($var === Manager::SESSION_UID_KEY) { + return false; + } elseif ($var === 'app_password') { + return false; + } elseif ($var === 'app_api') { + return false; + } + return true; + }); + $this->session->method('get') + ->willReturnCallback(function ($var) { + if ($var === Manager::SESSION_UID_KEY) { + return 'user'; + } elseif ($var === 'app_api') { + return true; + } + return null; + }); + $this->session->expects($this->once()) + ->method('get') + ->willReturnMap([ + [Manager::SESSION_UID_DONE, 'user'], + ['app_api', true] + ]); + + $this->assertFalse($this->manager->needsSecondFactor($user)); + } + + public function testNeedsSecondFactorSessionAuthFailDBPass(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->session->method('exists') + ->willReturn(false); + $this->session->method('getId') + ->willReturn('mysessionid'); + + $token = $this->createMock(IToken::class); + $token->method('getId') + ->willReturn(40); + + $this->tokenProvider->method('getToken') + ->with('mysessionid') + ->willReturn($token); + + $this->config->method('getUserKeys') + ->with('user', 'login_token_2fa') + ->willReturn([ + '42', '43', '44' + ]); + + $this->session->expects($this->once()) + ->method('set') + ->with(Manager::SESSION_UID_DONE, 'user'); + + $this->assertFalse($this->manager->needsSecondFactor($user)); + } + + public function testNeedsSecondFactorInvalidToken(): void { + $this->prepareNoProviders(); + + $user = $this->createMock(IUser::class); + $user->method('getUID') + ->willReturn('user'); + + $this->session->method('exists') + ->willReturn(false); + $this->session->method('getId') + ->willReturn('mysessionid'); + + $this->tokenProvider->method('getToken') + ->with('mysessionid') + ->willThrowException(new InvalidTokenException()); + + $this->config->method('getUserKeys')->willReturn([]); + + $this->assertFalse($this->manager->needsSecondFactor($user)); + } + + public function testNeedsSecondFactorAppPassword(): void { + $user = $this->createMock(IUser::class); + $this->session->method('exists') + ->willReturnMap([ + ['app_password', true], + ['app_api', true] + ]); + + $this->assertFalse($this->manager->needsSecondFactor($user)); + } + + public function testClearTwoFactorPending() { + $this->config->method('getUserKeys') + ->with('theUserId', 'login_token_2fa') + ->willReturn([ + '42', '43', '44' + ]); + + $deleteUserValueCalls = [ + ['theUserId', 'login_token_2fa', '42'], + ['theUserId', 'login_token_2fa', '43'], + ['theUserId', 'login_token_2fa', '44'], + ]; + $this->config->expects($this->exactly(3)) + ->method('deleteUserValue') + ->willReturnCallback(function () use (&$deleteUserValueCalls): void { + $expected = array_shift($deleteUserValueCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $invalidateCalls = [ + ['theUserId', 42], + ['theUserId', 43], + ['theUserId', 44], + ]; + $this->tokenProvider->expects($this->exactly(3)) + ->method('invalidateTokenById') + ->willReturnCallback(function () use (&$invalidateCalls): void { + $expected = array_shift($invalidateCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->manager->clearTwoFactorPending('theUserId'); + } + + public function testClearTwoFactorPendingTokenDoesNotExist() { + $this->config->method('getUserKeys') + ->with('theUserId', 'login_token_2fa') + ->willReturn([ + '42', '43', '44' + ]); + + $deleteUserValueCalls = [ + ['theUserId', 'login_token_2fa', '42'], + ['theUserId', 'login_token_2fa', '43'], + ['theUserId', 'login_token_2fa', '44'], + ]; + $this->config->expects($this->exactly(3)) + ->method('deleteUserValue') + ->willReturnCallback(function () use (&$deleteUserValueCalls): void { + $expected = array_shift($deleteUserValueCalls); + $this->assertEquals($expected, func_get_args()); + }); + + $invalidateCalls = [ + ['theUserId', 42], + ['theUserId', 43], + ['theUserId', 44], + ]; + $this->tokenProvider->expects($this->exactly(3)) + ->method('invalidateTokenById') + ->willReturnCallback(function ($user, $tokenId) use (&$invalidateCalls): void { + $expected = array_shift($invalidateCalls); + $this->assertEquals($expected, func_get_args()); + if ($tokenId === 43) { + throw new DoesNotExistException('token does not exist'); + } + }); + + $this->manager->clearTwoFactorPending('theUserId'); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/MandatoryTwoFactorTest.php b/tests/lib/Authentication/TwoFactorAuth/MandatoryTwoFactorTest.php new file mode 100644 index 00000000000..d2ecd3c509c --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/MandatoryTwoFactorTest.php @@ -0,0 +1,179 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Tests\Authentication\TwoFactorAuth; + +use OC\Authentication\TwoFactorAuth\EnforcementState; +use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class MandatoryTwoFactorTest extends TestCase { + /** @var IConfig|MockObject */ + private $config; + + /** @var IGroupManager|MockObject */ + private $groupManager; + + /** @var MandatoryTwoFactor */ + private $mandatoryTwoFactor; + + protected function setUp(): void { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->groupManager = $this->createMock(IGroupManager::class); + + $this->mandatoryTwoFactor = new MandatoryTwoFactor($this->config, $this->groupManager); + } + + public function testIsNotEnforced(): void { + $this->config + ->method('getSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'false', 'false'], + ['twofactor_enforced_groups', [], []], + ['twofactor_enforced_excluded_groups', [], []], + ]); + + $state = $this->mandatoryTwoFactor->getState(); + + $this->assertFalse($state->isEnforced()); + } + + public function testIsEnforced(): void { + $this->config + ->method('getSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'false', 'true'], + ['twofactor_enforced_groups', [], []], + ['twofactor_enforced_excluded_groups', [], []], + ]); + + $state = $this->mandatoryTwoFactor->getState(); + + $this->assertTrue($state->isEnforced()); + } + + public function testIsNotEnforcedForAnybody(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $this->config + ->method('getSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'false', 'false'], + ['twofactor_enforced_groups', [], []], + ['twofactor_enforced_excluded_groups', [], []], + ]); + + $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user); + + $this->assertFalse($isEnforced); + } + + public function testIsEnforcedForAGroupMember(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $this->config + ->method('getSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'false', 'true'], + ['twofactor_enforced_groups', [], ['twofactorers']], + ['twofactor_enforced_excluded_groups', [], []], + ]); + $this->groupManager->method('isInGroup') + ->willReturnCallback(function ($user, $group) { + return $user === 'user123' && $group === 'twofactorers'; + }); + + $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user); + + $this->assertTrue($isEnforced); + } + + public function testIsEnforcedForOtherGroups(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $this->config + ->method('getSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'false', 'true'], + ['twofactor_enforced_groups', [], ['twofactorers']], + ['twofactor_enforced_excluded_groups', [], []], + ]); + $this->groupManager->method('isInGroup') + ->willReturn(false); + + $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user); + + $this->assertFalse($isEnforced); + } + + public function testIsEnforcedButMemberOfExcludedGroup(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('user123'); + $this->config + ->method('getSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'false', 'true'], + ['twofactor_enforced_groups', [], []], + ['twofactor_enforced_excluded_groups', [], ['yoloers']], + ]); + $this->groupManager->method('isInGroup') + ->willReturnCallback(function ($user, $group) { + return $user === 'user123' && $group === 'yoloers'; + }); + + $isEnforced = $this->mandatoryTwoFactor->isEnforcedFor($user); + + $this->assertFalse($isEnforced); + } + + public function testSetEnforced(): void { + $this->config + ->expects($this->exactly(3)) + ->method('setSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'true'], + ['twofactor_enforced_groups', []], + ['twofactor_enforced_excluded_groups', []], + ]); + + $this->mandatoryTwoFactor->setState(new EnforcementState(true)); + } + + public function testSetEnforcedForGroups(): void { + $this->config + ->expects($this->exactly(3)) + ->method('setSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'true'], + ['twofactor_enforced_groups', ['twofactorers']], + ['twofactor_enforced_excluded_groups', ['yoloers']], + ]); + + $this->mandatoryTwoFactor->setState(new EnforcementState(true, ['twofactorers'], ['yoloers'])); + } + + public function testSetNotEnforced(): void { + $this->config + ->expects($this->exactly(3)) + ->method('setSystemValue') + ->willReturnMap([ + ['twofactor_enforced', 'false'], + ['twofactor_enforced_groups', []], + ['twofactor_enforced_excluded_groups', []], + ]); + + $this->mandatoryTwoFactor->setState(new EnforcementState(false)); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php b/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php new file mode 100644 index 00000000000..6eb3b7dfb26 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/ProviderLoaderTest.php @@ -0,0 +1,120 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\Authentication\TwoFactorAuth; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\AppFramework\Bootstrap\RegistrationContext; +use OC\AppFramework\Bootstrap\ServiceRegistration; +use OC\Authentication\TwoFactorAuth\ProviderLoader; +use OCP\App\IAppManager; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ProviderLoaderTest extends TestCase { + /** @var IAppManager|MockObject */ + private $appManager; + + /** @var IUser|MockObject */ + private $user; + + /** @var RegistrationContext|MockObject */ + private $registrationContext; + + /** @var ProviderLoader */ + private $loader; + + protected function setUp(): void { + parent::setUp(); + + $this->appManager = $this->createMock(IAppManager::class); + $this->user = $this->createMock(IUser::class); + + $this->registrationContext = $this->createMock(RegistrationContext::class); + $coordinator = $this->createMock(Coordinator::class); + $coordinator->method('getRegistrationContext') + ->willReturn($this->registrationContext); + + $this->loader = new ProviderLoader($this->appManager, $coordinator); + } + + + public function testFailHardIfProviderCanNotBeLoaded(): void { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Could not load two-factor auth provider \\OCA\\MyFaulty2faApp\\DoesNotExist'); + + $this->appManager->expects($this->once()) + ->method('getEnabledAppsForUser') + ->with($this->user) + ->willReturn(['mail', 'twofactor_totp']); + $this->appManager + ->method('getAppInfo') + ->willReturnMap([ + ['mail', false, null, []], + ['twofactor_totp', false, null, [ + 'two-factor-providers' => [ + '\\OCA\\MyFaulty2faApp\\DoesNotExist', + ], + ]], + ]); + + $this->loader->getProviders($this->user); + } + + public function testGetProviders(): void { + $provider = $this->createMock(IProvider::class); + $provider->method('getId')->willReturn('test'); + \OC::$server->registerService('\\OCA\\TwoFactorTest\\Provider', function () use ($provider) { + return $provider; + }); + $this->appManager->expects($this->once()) + ->method('getEnabledAppsForUser') + ->with($this->user) + ->willReturn(['twofactor_test']); + $this->appManager + ->method('getAppInfo') + ->with('twofactor_test') + ->willReturn(['two-factor-providers' => [ + '\\OCA\\TwoFactorTest\\Provider', + ]]); + + $providers = $this->loader->getProviders($this->user); + + $this->assertCount(1, $providers); + $this->assertArrayHasKey('test', $providers); + $this->assertSame($provider, $providers['test']); + } + + public function testGetProvidersBootstrap(): void { + $provider = $this->createMock(IProvider::class); + $provider->method('getId')->willReturn('test'); + + \OC::$server->registerService('\\OCA\\TwoFactorTest\\Provider', function () use ($provider) { + return $provider; + }); + + $this->appManager->expects($this->once()) + ->method('getEnabledAppsForUser') + ->with($this->user) + ->willReturn([]); + + $this->registrationContext->method('getTwoFactorProviders') + ->willReturn([ + new ServiceRegistration('twofactor_test', '\\OCA\\TwoFactorTest\\Provider') + ]); + + $providers = $this->loader->getProviders($this->user); + + $this->assertCount(1, $providers); + $this->assertArrayHasKey('test', $providers); + $this->assertSame($provider, $providers['test']); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/ProviderManagerTest.php b/tests/lib/Authentication/TwoFactorAuth/ProviderManagerTest.php new file mode 100644 index 00000000000..a1f2a6fa69a --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/ProviderManagerTest.php @@ -0,0 +1,136 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace lib\Authentication\TwoFactorAuth; + +use OC\Authentication\Exceptions\InvalidProviderException; +use OC\Authentication\TwoFactorAuth\ProviderLoader; +use OC\Authentication\TwoFactorAuth\ProviderManager; +use OCP\Authentication\TwoFactorAuth\IActivatableByAdmin; +use OCP\Authentication\TwoFactorAuth\IDeactivatableByAdmin; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\Authentication\TwoFactorAuth\IRegistry; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class ProviderManagerTest extends TestCase { + /** @var ProviderLoader|MockObject */ + private $providerLoader; + + /** @var IRegistry|MockObject */ + private $registry; + + /** @var ProviderManager */ + private $providerManager; + + protected function setUp(): void { + parent::setUp(); + + $this->providerLoader = $this->createMock(ProviderLoader::class); + $this->registry = $this->createMock(IRegistry::class); + + $this->providerManager = new ProviderManager( + $this->providerLoader, + $this->registry + ); + } + + + public function testTryEnableInvalidProvider(): void { + $this->expectException(InvalidProviderException::class); + + $user = $this->createMock(IUser::class); + $this->providerManager->tryEnableProviderFor('none', $user); + } + + public function testTryEnableUnsupportedProvider(): void { + $user = $this->createMock(IUser::class); + $provider = $this->createMock(IProvider::class); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($user) + ->willReturn([ + 'u2f' => $provider, + ]); + $this->registry->expects($this->never()) + ->method('enableProviderFor'); + + $res = $this->providerManager->tryEnableProviderFor('u2f', $user); + + $this->assertFalse($res); + } + + public function testTryEnableProvider(): void { + $user = $this->createMock(IUser::class); + $provider = $this->createMock(IActivatableByAdmin::class); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($user) + ->willReturn([ + 'u2f' => $provider, + ]); + $provider->expects($this->once()) + ->method('enableFor') + ->with($user); + $this->registry->expects($this->once()) + ->method('enableProviderFor') + ->with($provider, $user); + + $res = $this->providerManager->tryEnableProviderFor('u2f', $user); + + $this->assertTrue($res); + } + + + public function testTryDisableInvalidProvider(): void { + $this->expectException(InvalidProviderException::class); + + $user = $this->createMock(IUser::class); + $this->providerManager->tryDisableProviderFor('none', $user); + } + + public function testTryDisableUnsupportedProvider(): void { + $user = $this->createMock(IUser::class); + $provider = $this->createMock(IProvider::class); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($user) + ->willReturn([ + 'u2f' => $provider, + ]); + $this->registry->expects($this->never()) + ->method('disableProviderFor'); + + $res = $this->providerManager->tryDisableProviderFor('u2f', $user); + + $this->assertFalse($res); + } + + public function testTryDisableProvider(): void { + $user = $this->createMock(IUser::class); + $provider = $this->createMock(IDeactivatableByAdmin::class); + $this->providerLoader->expects($this->once()) + ->method('getProviders') + ->with($user) + ->willReturn([ + 'u2f' => $provider, + ]); + $provider->expects($this->once()) + ->method('disableFor') + ->with($user); + $this->registry->expects($this->once()) + ->method('disableProviderFor') + ->with($provider, $user); + + $res = $this->providerManager->tryDisableProviderFor('u2f', $user); + + $this->assertTrue($res); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php b/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php new file mode 100644 index 00000000000..568b83567f8 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/ProviderSetTest.php @@ -0,0 +1,75 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\TwoFactorAuth; + +use OC\Authentication\TwoFactorAuth\ProviderSet; +use OCA\TwoFactorBackupCodes\Provider\BackupCodesProvider; +use OCP\Authentication\TwoFactorAuth\IProvider; +use Test\TestCase; + +class ProviderSetTest extends TestCase { + /** @var ProviderSet */ + private $providerSet; + + public function testIndexesProviders(): void { + $p1 = $this->createMock(IProvider::class); + $p1->method('getId')->willReturn('p1'); + $p2 = $this->createMock(IProvider::class); + $p2->method('getId')->willReturn('p2'); + $expected = [ + 'p1' => $p1, + 'p2' => $p2, + ]; + + $set = new ProviderSet([$p2, $p1], false); + + $this->assertEquals($expected, $set->getProviders()); + } + + public function testGet3rdPartyProviders(): void { + $p1 = $this->createMock(IProvider::class); + $p1->method('getId')->willReturn('p1'); + $p2 = $this->createMock(IProvider::class); + $p2->method('getId')->willReturn('p2'); + $p3 = $this->createMock(BackupCodesProvider::class); + $p3->method('getId')->willReturn('p3'); + $expected = [ + 'p1' => $p1, + 'p2' => $p2, + ]; + + $set = new ProviderSet([$p2, $p1], false); + + $this->assertEquals($expected, $set->getPrimaryProviders()); + } + + public function testGetProvider(): void { + $p1 = $this->createMock(IProvider::class); + $p1->method('getId')->willReturn('p1'); + + $set = new ProviderSet([$p1], false); + $provider = $set->getProvider('p1'); + + $this->assertEquals($p1, $provider); + } + + public function testGetProviderNotFound(): void { + $set = new ProviderSet([], false); + $provider = $set->getProvider('p1'); + + $this->assertNull($provider); + } + + public function testIsProviderMissing(): void { + $set = new ProviderSet([], true); + + $this->assertTrue($set->isProviderMissing()); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/RegistryTest.php b/tests/lib/Authentication/TwoFactorAuth/RegistryTest.php new file mode 100644 index 00000000000..2018dc1a634 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/RegistryTest.php @@ -0,0 +1,144 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Test\Authentication\TwoFactorAuth; + +use OC\Authentication\TwoFactorAuth\Db\ProviderUserAssignmentDao; +use OC\Authentication\TwoFactorAuth\Registry; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\Authentication\TwoFactorAuth\IRegistry; +use OCP\Authentication\TwoFactorAuth\RegistryEvent; +use OCP\Authentication\TwoFactorAuth\TwoFactorProviderDisabled; +use OCP\Authentication\TwoFactorAuth\TwoFactorProviderForUserRegistered; +use OCP\Authentication\TwoFactorAuth\TwoFactorProviderForUserUnregistered; +use OCP\Authentication\TwoFactorAuth\TwoFactorProviderUserDeleted; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +class RegistryTest extends TestCase { + /** @var ProviderUserAssignmentDao|MockObject */ + private $dao; + + /** @var IEventDispatcher|MockObject */ + private $dispatcher; + + /** @var Registry */ + private $registry; + + protected function setUp(): void { + parent::setUp(); + + $this->dao = $this->createMock(ProviderUserAssignmentDao::class); + $this->dispatcher = $this->createMock(IEventDispatcher::class); + + $this->registry = new Registry($this->dao, $this->dispatcher); + } + + public function testGetProviderStates(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('user123'); + $state = [ + 'twofactor_totp' => true, + ]; + $this->dao->expects($this->once())->method('getState')->willReturn($state); + + $actual = $this->registry->getProviderStates($user); + + $this->assertEquals($state, $actual); + } + + public function testEnableProvider(): void { + $user = $this->createMock(IUser::class); + $provider = $this->createMock(IProvider::class); + $user->expects($this->once())->method('getUID')->willReturn('user123'); + $provider->expects($this->once())->method('getId')->willReturn('p1'); + $this->dao->expects($this->once())->method('persist')->with('p1', 'user123', + true); + + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with( + $this->equalTo(IRegistry::EVENT_PROVIDER_ENABLED), + $this->callback(function (RegistryEvent $e) use ($user, $provider) { + return $e->getUser() === $user && $e->getProvider() === $provider; + }) + ); + $this->dispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with(new TwoFactorProviderForUserRegistered( + $user, + $provider, + )); + + $this->registry->enableProviderFor($provider, $user); + } + + public function testDisableProvider(): void { + $user = $this->createMock(IUser::class); + $provider = $this->createMock(IProvider::class); + $user->expects($this->once())->method('getUID')->willReturn('user123'); + $provider->expects($this->once())->method('getId')->willReturn('p1'); + $this->dao->expects($this->once())->method('persist')->with('p1', 'user123', + false); + + + $this->dispatcher->expects($this->once()) + ->method('dispatch') + ->with( + $this->equalTo(IRegistry::EVENT_PROVIDER_DISABLED), + $this->callback(function (RegistryEvent $e) use ($user, $provider) { + return $e->getUser() === $user && $e->getProvider() === $provider; + }) + ); + $this->dispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with(new TwoFactorProviderForUserUnregistered( + $user, + $provider, + )); + + $this->registry->disableProviderFor($provider, $user); + } + + public function testDeleteUserData(): void { + $user = $this->createMock(IUser::class); + $user->expects($this->once())->method('getUID')->willReturn('user123'); + $this->dao->expects($this->once()) + ->method('deleteByUser') + ->with('user123') + ->willReturn([ + [ + 'provider_id' => 'twofactor_u2f', + ] + ]); + + $calls = [ + [new TwoFactorProviderDisabled('twofactor_u2f')], + [new TwoFactorProviderUserDeleted($user, 'twofactor_u2f')], + ]; + $this->dispatcher->expects($this->exactly(2)) + ->method('dispatchTyped') + ->willReturnCallback(function () use (&$calls): void { + $expected = array_shift($calls); + $this->assertEquals($expected, func_get_args()); + }); + + $this->registry->deleteUserData($user); + } + + public function testCleanUp(): void { + $this->dao->expects($this->once()) + ->method('deleteAll') + ->with('twofactor_u2f'); + + $this->registry->cleanUp('twofactor_u2f'); + } +} |