From 9f6a4bd67f8eefdff6414a7151c5a859a7fe8844 Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Fri, 5 Mar 2021 20:15:18 +0100
Subject: [PATCH] Implemented rejected join persistence

---
 classes/datalayer/UserDAO.php            |   4 +-
 classes/datalayer/mysql/MySQLUserDAO.php |  22 ++-
 classes/login/LoginHandler.php           |  39 ++++--
 tests/LoginFlowTest.php                  | 169 ++++++++++++++++++++---
 tests/LoginHandlerTest.php               |  24 +++-
 5 files changed, 215 insertions(+), 43 deletions(-)

diff --git a/classes/datalayer/UserDAO.php b/classes/datalayer/UserDAO.php
index 436f755..0eb2f6a 100644
--- a/classes/datalayer/UserDAO.php
+++ b/classes/datalayer/UserDAO.php
@@ -81,6 +81,8 @@ interface UserDAO {
     function updateIdentity(Identity $identity): void;
 
     function findJoinableUsersByEmail(string $email): array;
-    
+
     function findJoinableUsersByUserId(string $userId): array;
+
+    function insertRejectedJoin(string $userId1, string $userId2): void;
 }
diff --git a/classes/datalayer/mysql/MySQLUserDAO.php b/classes/datalayer/mysql/MySQLUserDAO.php
index d65bc7b..f16ce49 100644
--- a/classes/datalayer/mysql/MySQLUserDAO.php
+++ b/classes/datalayer/mysql/MySQLUserDAO.php
@@ -302,9 +302,9 @@ class MySQLUserDAO extends BaseMySQLDAO implements UserDAO {
     }
 
     function findJoinableUsersByEmail(string $email): array {
-        
+
         $dbh = $this->getDBHandler();
-        
+
         $query = "SELECT DISTINCT(i3.user_id)\n"
                 . "FROM user u\n"
                 . "JOIN identity i ON i.user_id = u.id\n"
@@ -323,11 +323,11 @@ class MySQLUserDAO extends BaseMySQLDAO implements UserDAO {
         }
         return $results;
     }
-    
+
     function findJoinableUsersByUserId(string $userId): array {
 
         $dbh = $this->getDBHandler();
-        
+
         $query = "SELECT DISTINCT(i3.user_id)\n"
                 . "FROM user u\n"
                 . "JOIN identity i ON i.user_id = u.id\n"
@@ -351,4 +351,18 @@ class MySQLUserDAO extends BaseMySQLDAO implements UserDAO {
         return $results;
     }
 
+    function insertRejectedJoin(string $userId1, string $userId2): void {
+
+        $dbh = $this->getDBHandler();
+
+        $query = "INSERT INTO keep_separated (user_id1, user_id2) VALUES (:id1, :id2)\n"
+                . "ON DUPLICATE KEY UPDATE user_id1 = user_id1, user_id2 = user_id2";
+
+        $stmt = $dbh->prepare($query);
+        $stmt->bindParam(':id1', $userId1);
+        $stmt->bindParam(':id2', $userId2);
+
+        $stmt->execute();
+    }
+
 }
diff --git a/classes/login/LoginHandler.php b/classes/login/LoginHandler.php
index 571421e..b480f27 100644
--- a/classes/login/LoginHandler.php
+++ b/classes/login/LoginHandler.php
@@ -78,11 +78,6 @@ class LoginHandler {
 
         $this->joinUsers();
 
-        $autoJoinStep = $this->checkAutoJoin();
-        if ($autoJoinStep !== null) {
-            return $autoJoinStep;
-        }
-
         return $this->getAfterLoginRedirect();
     }
 
@@ -105,6 +100,7 @@ class LoginHandler {
             return $this->showConfirmJoin($userToJoin);
         }
 
+        $session->setAutojoin(false);
         return null;
     }
 
@@ -120,8 +116,9 @@ class LoginHandler {
         $session->setJoinRejected(true);
 
         if ($session->getUser()->id === null) {
-            return $this->redirectToTOUCheck($session->getUser()->identities[0]);
+            return $this->redirectToTOUCheck();
         } else {
+            $this->saveRejectedJoinIfPossible();
             return $this->getAfterLoginRedirect();
         }
     }
@@ -158,12 +155,8 @@ class LoginHandler {
         $user = $session->getUser();
         $userToJoin = $session->getUserToJoin();
 
-        if ($user === null) {
-            $session->setUser($userToJoin);
-        } else {
-            $joinedUser = $this->locator->getUserHandler()->joinUsers($userToJoin, $user);
-            $session->setUser($joinedUser);
-        }
+        $joinedUser = $this->locator->getUserHandler()->joinUsers($userToJoin, $user);
+        $session->setUser($joinedUser);
 
         if ($session->getAction() === 'join') {
             $session->setAction('account');
@@ -174,7 +167,15 @@ class LoginHandler {
 
     private function getAfterLoginRedirect(): string {
 
+        $autoJoinStep = $this->checkAutoJoin();
+        if ($autoJoinStep !== null) {
+            return $autoJoinStep;
+        }
+
         $session = $this->locator->getSession();
+
+        $this->saveRejectedJoinIfPossible();
+
         $this->locator->getAuditLogger()->info("LOGIN," . $session->getLoginIdentityType() . "," . $session->getUser()->id);
 
         if ($session->getOAuth2RequestData() !== null) {
@@ -193,4 +194,18 @@ class LoginHandler {
         throw new \Exception("Unable to find a proper redirect");
     }
 
+    private function saveRejectedJoinIfPossible(): void {
+
+        $session = $this->locator->getSession();
+
+        if ($session->isJoinRejected() && $session->getUserToJoin() !== null) {
+            $id1 = $session->getUser()->id;
+            $id2 = $session->getUserToJoin()->id;
+            if ($id1 !== null && $id2 !== null) {
+                $this->locator->getUserDAO()->insertRejectedJoin($id1, $id2);
+                $session->setJoinRejected(false);
+            }
+        }
+    }
+
 }
diff --git a/tests/LoginFlowTest.php b/tests/LoginFlowTest.php
index 80b3086..e784ecb 100644
--- a/tests/LoginFlowTest.php
+++ b/tests/LoginFlowTest.php
@@ -37,20 +37,20 @@ final class LoginFlowTest extends TestCase {
 
         $this->gmsClientStub = $this->createMock(\RAP\GmsClient::class);
         $this->locatorStub->method('getGmsClient')->willReturn($this->gmsClientStub);
-        
+
         $this->sessionData = new \RAP\SessionData();
         $this->locatorStub->method('getSession')->willReturn($this->sessionData);
 
         $this->userHandler = new \RAP\UserHandler($this->locatorStub);
         $this->locatorStub->method('getUserHandler')->willReturn($this->userHandler);
-        
+
         $this->loginHandler = new \RAP\LoginHandler($this->locatorStub);
     }
 
     public function testNewIdentityLogin(): void {
 
         $this->sessionData->setAction('account');
-        
+
         $identity = $this->getFakeIdentity1();
 
         // verify DAO is called for checking user existence
@@ -79,7 +79,7 @@ final class LoginFlowTest extends TestCase {
     public function testExistingUserLogin(): void {
 
         $this->sessionData->setAction('account');
-        
+
         $user = $this->getFakeUser1();
         $this->assertEquals('test@example.com', $user->identities[0]->email);
 
@@ -95,35 +95,43 @@ final class LoginFlowTest extends TestCase {
 
         $this->assertEquals('http://rap-ia2/account', $redirect);
     }
-    
+
     public function testNewIdentityAutojoin(): void {
-        
+
         $this->oAuth2RequestHandler->method('getRedirectResponseUrl')->willReturn('http://redirect-url');
-        
+        $this->sessionData->setOAuth2RequestData(new \RAP\OAuth2RequestData());
+
         $this->userDaoStub->method('findJoinableUsersByEmail')->willReturn(['1', '2']);
-        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());
-        
+
+        $this->userDaoStub->method('findUserById')->will($this->returnValueMap([
+                    ['1', $this->getFakeUser1()],
+                    ['2', $this->getFakeUser2()]
+        ]));
+
+        // Login: two joinable users detected
         $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity3());
-        
+
         $this->assertTrue($this->sessionData->isAutojoin());
         $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);
         $this->assertNotNull($this->sessionData->getUserToJoin());
         $this->assertNull($this->sessionData->getUser()->id);
-        
-        $this->userDaoStub->method('findJoinableUsersByUserId')->willReturn(['2']);
-        
+
+        $this->userDaoStub->method('findJoinableUsersByUserId')->will($this->onConsecutiveCalls(['2'], []));
+
+        // First confirm join
         $redirect2 = $this->loginHandler->confirmJoin();
-                
+
         $this->assertEquals('1', $this->sessionData->getUser()->id);
         $this->assertEquals('http://rap-ia2/confirm-join', $redirect2);
-                
+
         $this->gmsClientStub->method('joinGroups')->willReturn('2');
-        
+
         $this->gmsClientStub->expects($this->once())
                 ->method('joinGroups')->with($this->anything());
-        
+
+        // Second confirm join, then redirect to caller application
         $redirect3 = $this->loginHandler->confirmJoin();
-        
+
         $this->assertEquals('2', $this->sessionData->getUser()->id);
         $this->assertEquals('http://redirect-url', $redirect3);
     }
@@ -141,6 +149,129 @@ final class LoginFlowTest extends TestCase {
         $this->assertEquals('http://rap-ia2/account', $redirect);
     }
 
+    public function testRejectJoinExistingUser(): void {
+
+        $this->sessionData->setAction('account');
+
+        $this->userDaoStub->method('findUserByIdentity')->willReturn($this->getFakeUser1());
+        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser2());
+        $this->userDaoStub->method('findJoinableUsersByUserId')->will($this->onConsecutiveCalls(['2'], []));
+
+        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());
+
+        $this->assertTrue($this->sessionData->isAutojoin());
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);
+
+        $this->userDaoStub->expects($this->once())->method('insertRejectedJoin');
+
+        // User rejects join, redirect to account page
+        $redirect2 = $this->loginHandler->rejectJoin();
+
+        $this->assertEquals('http://rap-ia2/account', $redirect2);
+    }
+
+    public function testRejectJoinNewUser(): void {
+
+        $this->sessionData->setAction('account');
+
+        $this->userDaoStub->method('findJoinableUsersByEmail')->willReturn(['1']);
+        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());
+
+        // Login: one joinable user detected
+        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity3());
+
+        $this->assertTrue($this->sessionData->isAutojoin());
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);
+
+        // User rejects join, redirect to TOU check
+        $redirect2 = $this->loginHandler->rejectJoin();
+
+        $this->assertTrue($this->sessionData->isJoinRejected());
+        $this->assertEquals('http://rap-ia2/tou-check', $redirect2);
+
+        $this->userDaoStub->method('createUser')->willReturn('5');
+        $this->userDaoStub->expects($this->once())->method('insertRejectedJoin');
+
+        // User accepts TOU
+        $redirect3 = $this->loginHandler->register();
+        $this->assertEquals('http://rap-ia2/account', $redirect3);
+    }
+
+    public function testExplicitJoin(): void {
+
+        // First login result
+        $this->sessionData->setUser($this->getFakeUser2());
+
+        $this->sessionData->setAction('join');
+
+        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());
+
+        // Second login
+        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());
+
+        $this->assertFalse($this->sessionData->isAutojoin());
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);
+
+        // User confirms the join
+        $redirect2 = $this->loginHandler->confirmJoin();
+        $this->assertEquals('http://rap-ia2/account', $redirect2);
+    }
+
+    public function testExplicitJoinAndAutojoin(): void {
+        // First login result
+        $this->sessionData->setUser($this->getFakeUser2());
+
+        $this->sessionData->setAction('join');
+
+        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());
+        $this->userDaoStub->method('findJoinableUsersByUserId')->will($this->onConsecutiveCalls(['2'], []));
+
+        // Second login
+        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());
+
+        $this->assertFalse($this->sessionData->isAutojoin());
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);
+
+        // User confirms the join, another join is detected
+        $redirect2 = $this->loginHandler->confirmJoin();
+
+        $this->assertTrue($this->sessionData->isAutojoin());
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect2);
+
+        // User confirm the second join too
+        $redirect3 = $this->loginHandler->confirmJoin();
+        $this->assertEquals('http://rap-ia2/account', $redirect3);
+    }
+
+    public function testExplicitJoinAndRejectedAutojoin(): void {
+
+        // First login result
+        $this->sessionData->setUser($this->getFakeUser2());
+
+        $this->sessionData->setAction('join');
+
+        $this->userDaoStub->method('findUserById')->willReturn($this->getFakeUser1());
+        $this->userDaoStub->method('findJoinableUsersByUserId')->will($this->onConsecutiveCalls(['2'], []));
+
+        // Second login
+        $redirect1 = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());
+
+        $this->assertFalse($this->sessionData->isAutojoin());
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect1);
+
+        // User confirms the join, another join is detected
+        $redirect2 = $this->loginHandler->confirmJoin();
+
+        $this->assertTrue($this->sessionData->isAutojoin());
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect2);
+
+        $this->userDaoStub->expects($this->once())->method('insertRejectedJoin');
+
+        // User reject the second join
+        $redirect3 = $this->loginHandler->rejectJoin();
+        $this->assertEquals('http://rap-ia2/account', $redirect3);
+    }
+
     private function getFakeUser1(): \RAP\User {
 
         $user = new \RAP\User();
@@ -152,7 +283,7 @@ final class LoginFlowTest extends TestCase {
         $user->addIdentity($identity);
         return $user;
     }
-    
+
     private function getFakeUser2(): \RAP\User {
 
         $user = new \RAP\User();
diff --git a/tests/LoginHandlerTest.php b/tests/LoginHandlerTest.php
index 3540aa4..7f7d690 100644
--- a/tests/LoginHandlerTest.php
+++ b/tests/LoginHandlerTest.php
@@ -12,7 +12,6 @@ final class LoginHandlerTest extends TestCase {
     private $userDaoStub;
     private $sessionStub;
     private $userHandlerStub;
-    private $oAuth2RequestHandler;
     private $auditLogger;
     private $loginHandler;
 
@@ -29,9 +28,6 @@ final class LoginHandlerTest extends TestCase {
         $this->userHandlerStub = $this->createMock(\RAP\UserHandler::class);
         $this->locatorStub->method('getUserHandler')->willReturn($this->userHandlerStub);
 
-        $this->oAuth2RequestHandler = $this->createMock(\RAP\OAuth2RequestHandler::class);
-        $this->locatorStub->method('getOAuth2RequestHandler')->willReturn($this->oAuth2RequestHandler);
-
         $this->auditLogger = $this->createMock(\Monolog\Logger::class);
         $this->locatorStub->method('getAuditLogger')->willReturn($this->auditLogger);
 
@@ -44,14 +40,13 @@ final class LoginHandlerTest extends TestCase {
 
         $this->userDaoStub->method('findUserByIdentity')->willReturn($user);
 
-        $this->oAuth2RequestHandler->method('getRedirectResponseUrl')->willReturn('http://redirect-url');
+        $this->sessionStub->method('getAction')->willReturn('account');
 
-        $this->sessionStub->method('getOAuth2RequestData')->willReturn(new \RAP\OAuth2RequestData());
         $this->sessionStub->method('getUser')->willReturn($user);
 
         $redirect = $this->loginHandler->onIdentityDataReceived($this->getFakeIdentity1());
 
-        $this->assertEquals('http://redirect-url', $redirect);
+        $this->assertEquals('http://rap-ia2/account', $redirect);
     }
 
     public function testShowConfirmJoinNewIdentity(): void {
@@ -85,6 +80,21 @@ final class LoginHandlerTest extends TestCase {
         $this->assertEquals('http://rap-ia2/confirm-join', $redirect);
     }
 
+    public function testRegisterFailWithoutSession(): void {
+        $this->expectException(\RAP\BadRequestException::class);
+        $this->loginHandler->register();
+    }
+
+    public function testConfirmJoinFailWithoutSession(): void {
+        $this->expectException(\RAP\BadRequestException::class);
+        $this->loginHandler->confirmJoin();
+    }
+
+    public function testRejectJoinFailWithoutSession(): void {
+        $this->expectException(\RAP\BadRequestException::class);
+        $this->loginHandler->rejectJoin();
+    }
+
     private function getFakeUser(): \RAP\User {
 
         $user = new \RAP\User();
-- 
GitLab