From b0a2278a7ec0309ff70ac5e5254c41030f4d1880 Mon Sep 17 00:00:00 2001 From: Konrad Abicht Date: Thu, 11 Feb 2021 09:38:29 +0100 Subject: [PATCH 1/6] added unit tests for LoginFlowV2Service::poll Signed-off-by: Konrad Abicht --- .../Service/LoginFlowV2ServiceUnitTest.php | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 tests/Core/Service/LoginFlowV2ServiceUnitTest.php diff --git a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php new file mode 100644 index 0000000000..ed87380897 --- /dev/null +++ b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php @@ -0,0 +1,215 @@ + + * + * @copyright Copyright (c) 2021, Konrad Abicht + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace Tests\Core\Data; + +use OC\Core\Service\LoginFlowV2Service; +use OC\Core\Db\LoginFlowV2Mapper; +use OC\Core\Exception\LoginFlowV2NotFoundException; +use OC\Authentication\Token\IProvider; +use OC\Core\Data\LoginFlowV2Credentials; +use OC\Core\Db\LoginFlowV2; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\ILogger; +use OCP\Security\ICrypto; +use OCP\Security\ISecureRandom; +use Test\TestCase; + +/** + * Unit tests for \OC\Core\Service\LoginFlowV2Service + */ +class LoginFlowV2ServiceUnitTest extends TestCase { + /** @var \OCP\IConfig */ + private $config; + + /** @var \OCP\Security\ICrypto */ + private $crypto; + + /** @var \OCP\ILogger */ + private $logger; + + /** @var \OC\Core\Db\LoginFlowV2Mapper */ + private $mapper; + + /** @var \OCP\Security\ISecureRandom */ + private $secureRandom; + + /** @var \OC\Core\Service\LoginFlowV2Service */ + private $subjectUnderTest; + + /** @var \OCP\AppFramework\Utility\ITimeFactory */ + private $timeFactory; + + /** @var \OC\Authentication\Token\IProvider */ + private $tokenProvider; + + public function setUp(): void { + parent::setUp(); + + $this->setupSubjectUnderTest(); + } + + /** + * Setup subject under test with mocked constructor arguments. + * + * Code was moved to separate function to keep setUp function small and clear. + */ + private function setupSubjectUnderTest(): void + { + $this->config = $this->getMockBuilder(IConfig::class) + ->disableOriginalConstructor()->getMock(); + + $this->crypto = $this->getMockBuilder(ICrypto::class) + ->disableOriginalConstructor()->getMock(); + + $this->mapper = $this->getMockBuilder(LoginFlowV2Mapper::class) + ->disableOriginalConstructor()->getMock(); + + $this->logger = $this->getMockBuilder(ILogger::class) + ->disableOriginalConstructor()->getMock(); + + $this->tokenProvider = $this->getMockBuilder(IProvider::class) + ->disableOriginalConstructor()->getMock(); + + $this->secureRandom = $this->getMockBuilder(ISecureRandom::class) + ->disableOriginalConstructor()->getMock(); + + $this->timeFactory = $this->getMockBuilder(ITimeFactory::class) + ->disableOriginalConstructor()->getMock(); + + $this->subjectUnderTest = new LoginFlowV2Service( + $this->mapper, + $this->secureRandom, + $this->timeFactory, + $this->config, + $this->crypto, + $this->logger, + $this->tokenProvider + ); + } + + /** + * + */ + private function getOpenSSLEncryptedAndPrivateKey(string $appPassword): array { + // Create the private and public key + $res = openssl_pkey_new([ + 'digest_alg' => 'md5', // take fast algorithm for testing purposes + 'private_key_bits' => 4096, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); + + // Extract the private key from $res + openssl_pkey_export($res, $privateKey); + + // Extract the public key from $res + $publicKey = openssl_pkey_get_details($res); + $publicKey = $publicKey['key']; + + // Encrypt the data to $encrypted using the public key + openssl_public_encrypt($appPassword, $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); + + return [$encrypted, $privateKey]; + } + + /* + * Tests for poll + */ + + public function testPollApptokenCouldNotBeDecrypted() { + $this->expectException(LoginFlowV2NotFoundException::class); + $this->expectExceptionMessage('Apptoken could not be decrypted'); + + /* + * Cannot be mocked, because functions like getLoginName are magic functions. + * To be able to call it, we have to use the real class here. + */ + $loginFlowV2 = new LoginFlowV2(); + $loginFlowV2->setLoginName('test'); + $loginFlowV2->setServer('test'); + $loginFlowV2->setAppPassword('test'); + $loginFlowV2->setPrivateKey('test'); + + $this->mapper->expects($this->once()) + ->method('getByPollToken') + ->willReturn($loginFlowV2); + + $this->subjectUnderTest->poll(''); + } + + public function testPollInvalidToken() { + $this->expectException(LoginFlowV2NotFoundException::class); + $this->expectExceptionMessage('Invalid token'); + + $this->mapper->expects($this->once()) + ->method('getByPollToken') + ->willThrowException(new DoesNotExistException('')); + + $this->subjectUnderTest->poll(''); + } + + public function testPollTokenNotYetReady() { + $this->expectException(LoginFlowV2NotFoundException::class); + $this->expectExceptionMessage('Token not yet ready'); + + $this->subjectUnderTest->poll(''); + } + + public function testPollRemoveDataFromDb() { + list($encrypted, $privateKey) = $this->getOpenSSLEncryptedAndPrivateKey('test_pass'); + + $this->crypto->expects($this->once()) + ->method('decrypt') + ->willReturn($privateKey); + + /* + * Cannot be mocked, because functions like getLoginName are magic functions. + * To be able to call it, we have to use the real class here. + */ + $loginFlowV2 = new LoginFlowV2(); + $loginFlowV2->setLoginName('test_login'); + $loginFlowV2->setServer('test_server'); + $loginFlowV2->setAppPassword(base64_encode($encrypted)); + $loginFlowV2->setPrivateKey($privateKey); + + $this->mapper->expects($this->once()) + ->method('delete') + ->with($this->equalTo($loginFlowV2)); + + $this->mapper->expects($this->once()) + ->method('getByPollToken') + ->willReturn($loginFlowV2); + + $credentials = $this->subjectUnderTest->poll(''); + + $this->assertTrue($credentials instanceof LoginFlowV2Credentials); + $this->assertEquals( + [ + 'server' => 'test_server', + 'loginName' => 'test_login', + 'appPassword' => 'test_pass', + ], + $credentials->jsonSerialize() + ); + } +} From f29748a5e11bc1a86e47ce97dd00f6aaff183c36 Mon Sep 17 00:00:00 2001 From: Konrad Abicht Date: Thu, 11 Feb 2021 09:49:39 +0100 Subject: [PATCH 2/6] added unit tests for LoginFlowV2Service::getByLoginToken Signed-off-by: Konrad Abicht --- .../Service/LoginFlowV2ServiceUnitTest.php | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php index ed87380897..511c9be327 100644 --- a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php +++ b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php @@ -74,8 +74,7 @@ class LoginFlowV2ServiceUnitTest extends TestCase { * * Code was moved to separate function to keep setUp function small and clear. */ - private function setupSubjectUnderTest(): void - { + private function setupSubjectUnderTest(): void { $this->config = $this->getMockBuilder(IConfig::class) ->disableOriginalConstructor()->getMock(); @@ -115,7 +114,7 @@ class LoginFlowV2ServiceUnitTest extends TestCase { // Create the private and public key $res = openssl_pkey_new([ 'digest_alg' => 'md5', // take fast algorithm for testing purposes - 'private_key_bits' => 4096, + 'private_key_bits' => 512, 'private_key_type' => OPENSSL_KEYTYPE_RSA, ]); @@ -142,7 +141,7 @@ class LoginFlowV2ServiceUnitTest extends TestCase { /* * Cannot be mocked, because functions like getLoginName are magic functions. - * To be able to call it, we have to use the real class here. + * To be able to set internal properties, we have to use the real class here. */ $loginFlowV2 = new LoginFlowV2(); $loginFlowV2->setLoginName('test'); @@ -184,7 +183,7 @@ class LoginFlowV2ServiceUnitTest extends TestCase { /* * Cannot be mocked, because functions like getLoginName are magic functions. - * To be able to call it, we have to use the real class here. + * To be able to set internal properties, we have to use the real class here. */ $loginFlowV2 = new LoginFlowV2(); $loginFlowV2->setLoginName('test_login'); @@ -212,4 +211,37 @@ class LoginFlowV2ServiceUnitTest extends TestCase { $credentials->jsonSerialize() ); } + + /* + * Tests for getByLoginToken + */ + + public function testGetByLoginToken() { + $loginFlowV2 = new LoginFlowV2(); + $loginFlowV2->setLoginName('test_login'); + $loginFlowV2->setServer('test_server'); + $loginFlowV2->setAppPassword('test'); + + $this->mapper->expects($this->once()) + ->method('getByLoginToken') + ->willReturn($loginFlowV2); + + $result = $this->subjectUnderTest->getByLoginToken('test_token'); + + $this->assertTrue($result instanceof LoginFlowV2); + $this->assertEquals('test_server', $result->getServer()); + $this->assertEquals('test_login', $result->getLoginName()); + $this->assertEquals('test', $result->getAppPassword()); + } + + public function testGetByLoginTokenLoginTokenInvalid() { + $this->expectException(LoginFlowV2NotFoundException::class); + $this->expectExceptionMessage('Login token invalid'); + + $this->mapper->expects($this->once()) + ->method('getByLoginToken') + ->willThrowException(new DoesNotExistException('')); + + $this->subjectUnderTest->getByLoginToken('test_token'); + } } From d60dd8a20855440625feca3758968b784b5fca9d Mon Sep 17 00:00:00 2001 From: Konrad Abicht Date: Thu, 11 Feb 2021 09:56:09 +0100 Subject: [PATCH 3/6] added unit tests for LoginFlowV2Service::startLoginFlow Signed-off-by: Konrad Abicht --- .../Service/LoginFlowV2ServiceUnitTest.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php index 511c9be327..58c558aada 100644 --- a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php +++ b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php @@ -21,6 +21,7 @@ namespace Tests\Core\Data; +use Exception; use OC\Core\Service\LoginFlowV2Service; use OC\Core\Db\LoginFlowV2Mapper; use OC\Core\Exception\LoginFlowV2NotFoundException; @@ -244,4 +245,43 @@ class LoginFlowV2ServiceUnitTest extends TestCase { $this->subjectUnderTest->getByLoginToken('test_token'); } + + /* + * Tests for startLoginFlow + */ + + public function testStartLoginFlow() { + $loginFlowV2 = new LoginFlowV2(); + + $this->mapper->expects($this->once()) + ->method('getByLoginToken') + ->willReturn($loginFlowV2); + + $this->mapper->expects($this->once()) + ->method('update'); + + $this->assertTrue($this->subjectUnderTest->startLoginFlow('test_token')); + } + + public function testStartLoginFlowDoesNotExistException() { + $this->mapper->expects($this->once()) + ->method('getByLoginToken') + ->willThrowException(new DoesNotExistException('')); + + $this->assertFalse($this->subjectUnderTest->startLoginFlow('test_token')); + } + + /** + * If an exception not of type DoesNotExistException is thrown, + * it is expected that it is not being handled by startLoginFlow. + */ + public function testStartLoginFlowException() { + $this->expectException(Exception::class); + + $this->mapper->expects($this->once()) + ->method('getByLoginToken') + ->willThrowException(new Exception('')); + + $this->subjectUnderTest->startLoginFlow('test_token'); + } } From c755165dd46e0bd8d8b70ed1cd2e937795fbcdc9 Mon Sep 17 00:00:00 2001 From: Konrad Abicht Date: Thu, 11 Feb 2021 10:45:47 +0100 Subject: [PATCH 4/6] added unit tests for LoginFlowV2Service::flowDone Signed-off-by: Konrad Abicht --- .../Service/LoginFlowV2ServiceUnitTest.php | 104 +++++++++++++++++- 1 file changed, 100 insertions(+), 4 deletions(-) diff --git a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php index 58c558aada..9089d81dc9 100644 --- a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php +++ b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php @@ -22,12 +22,14 @@ namespace Tests\Core\Data; use Exception; -use OC\Core\Service\LoginFlowV2Service; -use OC\Core\Db\LoginFlowV2Mapper; -use OC\Core\Exception\LoginFlowV2NotFoundException; +use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Token\IProvider; +use OC\Authentication\Token\IToken; use OC\Core\Data\LoginFlowV2Credentials; +use OC\Core\Db\LoginFlowV2Mapper; use OC\Core\Db\LoginFlowV2; +use OC\Core\Exception\LoginFlowV2NotFoundException; +use OC\Core\Service\LoginFlowV2Service; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; @@ -109,7 +111,9 @@ class LoginFlowV2ServiceUnitTest extends TestCase { } /** + * Generates for a given password required OpenSSL parts. * + * @return array Array contains encrypted password, private key and public key. */ private function getOpenSSLEncryptedAndPrivateKey(string $appPassword): array { // Create the private and public key @@ -129,7 +133,7 @@ class LoginFlowV2ServiceUnitTest extends TestCase { // Encrypt the data to $encrypted using the public key openssl_public_encrypt($appPassword, $encrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING); - return [$encrypted, $privateKey]; + return [$encrypted, $privateKey, $publicKey]; } /* @@ -284,4 +288,96 @@ class LoginFlowV2ServiceUnitTest extends TestCase { $this->subjectUnderTest->startLoginFlow('test_token'); } + + /* + * Tests for flowDone + */ + + public function testFlowDone() { + list(,, $publicKey) = $this->getOpenSSLEncryptedAndPrivateKey('test_pass'); + + $loginFlowV2 = new LoginFlowV2(); + $loginFlowV2->setPublicKey($publicKey); + $loginFlowV2->setClientName('client_name'); + + $this->mapper->expects($this->once()) + ->method('getByLoginToken') + ->willReturn($loginFlowV2); + + $this->mapper->expects($this->once()) + ->method('update'); + + $this->secureRandom->expects($this->once()) + ->method('generate') + ->with(72, ISecureRandom::CHAR_UPPER.ISecureRandom::CHAR_LOWER.ISecureRandom::CHAR_DIGITS) + ->willReturn('test_pass'); + + // session token + $sessionToken = $this->getMockBuilder(IToken::class)->disableOriginalConstructor()->getMock(); + $sessionToken->expects($this->once()) + ->method('getLoginName') + ->willReturn('login_name'); + + $this->tokenProvider->expects($this->once()) + ->method('getPassword') + ->willReturn('test_pass'); + + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->willReturn($sessionToken); + + $this->tokenProvider->expects($this->once()) + ->method('generateToken') + ->with( + 'test_pass', + 'user_id', + 'login_name', + 'test_pass', + 'client_name', + IToken::PERMANENT_TOKEN, + IToken::DO_NOT_REMEMBER + ); + + $result = $this->subjectUnderTest->flowDone( + 'login_token', + 'session_id', + 'server', + 'user_id' + ); + $this->assertTrue($result); + + // app password is encrypted and must look like: + // ZACZOOzxTpKz4+KXL5kZ/gCK0xvkaVi/8yzupAn6Ui6+5qCSKvfPKGgeDRKs0sivvSLzk/XSp811SZCZmH0Y3g== + $this->assertRegExp('/[a-zA-Z\/0-9+=]+/', $loginFlowV2->getAppPassword()); + + $this->assertEquals('server', $loginFlowV2->getServer()); + } + + public function testFlowDoneDoesNotExistException() { + $this->mapper->expects($this->once()) + ->method('getByLoginToken') + ->willThrowException(new DoesNotExistException('')); + + $result = $this->subjectUnderTest->flowDone( + 'login_token', + 'session_id', + 'server', + 'user_id' + ); + $this->assertFalse($result); + } + + public function testFlowDonePasswordlessTokenException() { + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->willThrowException(new InvalidTokenException('')); + + $result = $this->subjectUnderTest->flowDone( + 'login_token', + 'session_id', + 'server', + 'user_id' + ); + $this->assertFalse($result); + } } From 0bc49d67cdc09da25e828848735e41abef12a157 Mon Sep 17 00:00:00 2001 From: Konrad Abicht Date: Thu, 11 Feb 2021 10:58:44 +0100 Subject: [PATCH 5/6] added unit tests for LoginFlowV2Service::createTokens Signed-off-by: Konrad Abicht --- .../Service/LoginFlowV2ServiceUnitTest.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php index 9089d81dc9..ba0a3130af 100644 --- a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php +++ b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php @@ -26,6 +26,7 @@ use OC\Authentication\Exceptions\InvalidTokenException; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IToken; use OC\Core\Data\LoginFlowV2Credentials; +use OC\Core\Data\LoginFlowV2Tokens; use OC\Core\Db\LoginFlowV2Mapper; use OC\Core\Db\LoginFlowV2; use OC\Core\Exception\LoginFlowV2NotFoundException; @@ -380,4 +381,26 @@ class LoginFlowV2ServiceUnitTest extends TestCase { ); $this->assertFalse($result); } + + /* + * Tests for createTokens + */ + + public function testCreateTokens() { + $this->config->expects($this->exactly(2)) + ->method('getSystemValue') + ->willReturn($this->returnCallback(function ($key) { + // Note: \OCP\IConfig::getSystemValue returns either an array or string. + return 'openssl' == $key ? [] : ''; + })); + + $this->mapper->expects($this->once()) + ->method('insert'); + + $this->secureRandom->expects($this->exactly(2)) + ->method('generate'); + + $token = $this->subjectUnderTest->createTokens('user_agent'); + $this->assertTrue($token instanceof LoginFlowV2Tokens); + } } From 330315f03e27c8a2c155229063eaef2050060485 Mon Sep 17 00:00:00 2001 From: Konrad Abicht Date: Fri, 12 Feb 2021 13:01:37 +0100 Subject: [PATCH 6/6] refined name of getOpenSSLEncryptedAndPrivateKey Signed-off-by: Konrad Abicht --- tests/Core/Service/LoginFlowV2ServiceUnitTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php index ba0a3130af..233960ea97 100644 --- a/tests/Core/Service/LoginFlowV2ServiceUnitTest.php +++ b/tests/Core/Service/LoginFlowV2ServiceUnitTest.php @@ -116,7 +116,7 @@ class LoginFlowV2ServiceUnitTest extends TestCase { * * @return array Array contains encrypted password, private key and public key. */ - private function getOpenSSLEncryptedAndPrivateKey(string $appPassword): array { + private function getOpenSSLEncryptedPublicAndPrivateKey(string $appPassword): array { // Create the private and public key $res = openssl_pkey_new([ 'digest_alg' => 'md5', // take fast algorithm for testing purposes @@ -181,7 +181,7 @@ class LoginFlowV2ServiceUnitTest extends TestCase { } public function testPollRemoveDataFromDb() { - list($encrypted, $privateKey) = $this->getOpenSSLEncryptedAndPrivateKey('test_pass'); + list($encrypted, $privateKey) = $this->getOpenSSLEncryptedPublicAndPrivateKey('test_pass'); $this->crypto->expects($this->once()) ->method('decrypt') @@ -295,7 +295,7 @@ class LoginFlowV2ServiceUnitTest extends TestCase { */ public function testFlowDone() { - list(,, $publicKey) = $this->getOpenSSLEncryptedAndPrivateKey('test_pass'); + list(,, $publicKey) = $this->getOpenSSLEncryptedPublicAndPrivateKey('test_pass'); $loginFlowV2 = new LoginFlowV2(); $loginFlowV2->setPublicKey($publicKey);