diff --git a/classes/datalayer/UserDAO.php b/classes/datalayer/UserDAO.php
index da7064c583116bd843357f0ff7c212605f982fd2..ffd5695737fb110407f94353d980c594e8fd91e8 100644
--- a/classes/datalayer/UserDAO.php
+++ b/classes/datalayer/UserDAO.php
@@ -52,7 +52,7 @@ interface UserDAO {
      * @param type $typedId typed unique value used to search the identity in the database
      * @return RAP\User an user object, null if nothing was found.
      */
-    function findUserByIdentity($type, $typedId);
+    function findUserByIdentity($type, $typedId): ?User;
 
     /**
      * Retrieve a set of users matching a given search text.
diff --git a/classes/datalayer/mysql/MySQLUserDAO.php b/classes/datalayer/mysql/MySQLUserDAO.php
index 02f8b8a86940050cc949c9fb77d33e48ffbb6f02..6c2ed71b2225330d819e6aa328d54e9c5807ac03 100644
--- a/classes/datalayer/mysql/MySQLUserDAO.php
+++ b/classes/datalayer/mysql/MySQLUserDAO.php
@@ -123,7 +123,7 @@ class MySQLUserDAO extends BaseMySQLDAO implements UserDAO {
         $stmt->execute();
     }
 
-    public function findUserByIdentity($type, $identifier) {
+    public function findUserByIdentity($type, $identifier): ?User {
 
         $dbh = $this->getDBHandler();
 
diff --git a/tests/LoginHandlerTest.php b/tests/LoginHandlerTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c164267ee7e9de422d4e0ce9550c71016bb930fe
--- /dev/null
+++ b/tests/LoginHandlerTest.php
@@ -0,0 +1,157 @@
+<?php
+
+use PHPUnit\Framework\TestCase;
+
+session_start();
+
+final class LoginHandlerTest extends TestCase {
+
+    private $locatorStub;
+    private $userDaoStub;
+    private $sessionStub;
+    private $oAuth2RequestHandler;
+    private $auditLogger;
+    private $loginHandler;
+
+    public function setUp(): void {
+        $this->locatorStub = $this->createMock(\RAP\Locator::class);
+        $this->locatorStub->method('getBasePath')->willReturn('http://rap-ia2');
+
+        $this->userDaoStub = $this->createMock(\RAP\UserDAO::class);
+        $this->locatorStub->method('getUserDAO')->willReturn($this->userDaoStub);
+
+        $this->sessionStub = $this->createMock(\RAP\SessionData::class);
+        $this->locatorStub->method('getSession')->willReturn($this->sessionStub);
+
+        $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);
+
+        $this->loginHandler = new \RAP\LoginHandler($this->locatorStub, 'eduGAIN');
+    }
+
+    public function testTOUCheck(): void {
+        $redirect = $this->loginHandler->onIdentityDataReceived('123', function($identity) {
+            $identity->email = 'test@example.com';
+        });
+
+        $this->assertEquals('http://rap-ia2/tou-check', $redirect);
+    }
+
+    public function testExistingUserLogin(): void {
+
+        $user = $this->getFakeUser();
+        $this->assertEquals('test@example.com', $user->identities[0]->email);
+
+        $this->userDaoStub->method('findUserByIdentity')->willReturn($user);
+
+        $this->oAuth2RequestHandler->method('getRedirectResponseUrl')->willReturn('http://redirect-url');
+
+        $this->sessionStub->method('getOAuth2RequestData')->willReturn(new \RAP\OAuth2RequestData());
+
+        $this->userDaoStub->expects($this->once())
+                ->method('updateIdentity')->with($this->anything());
+
+        $redirect = $this->loginHandler->onIdentityDataReceived('123', function($identity) {
+            $identity->email = 'test2@example.com';
+        });
+
+        // verify update
+        $this->assertEquals('test2@example.com', $user->identities[0]->email);
+
+        $this->assertEquals('http://redirect-url', $redirect);
+    }
+
+    public function testShowConfirmJoinNewIdentity(): void {
+
+        $user = $this->getFakeUser();
+
+        $this->sessionStub->method('getUser')->willReturn($user);
+        $this->sessionStub->method('getAction')->willReturn('join');
+
+        $redirect = $this->loginHandler->onIdentityDataReceived('456', function($identity) {
+            $identity->email = 'test3@example.com';
+        });
+
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect);
+    }
+
+    public function testShowConfirmJoinExistingIdentity(): void {
+
+        $user1 = $this->getFakeUser();
+
+        $user2 = $this->getFakeUser();
+        $user2->id = '2';
+        $user2->identities[0]->id = '5';
+        $user2->identities[0]->typedId = '456';
+
+        $this->sessionStub->method('getUser')->willReturn($user1);
+        $this->sessionStub->method('getAction')->willReturn('join');
+
+        $this->userDaoStub->method('findUserByIdentity')->willReturn($user2);
+
+        $redirect = $this->loginHandler->onIdentityDataReceived('456', function($identity) {
+            $identity->email = 'test3@example.com';
+        });
+
+        $this->assertEquals('http://rap-ia2/confirm-join', $redirect);
+    }
+
+    public function testJoinExistingUser(): void {
+
+        $user = $this->getFakeUser();
+        $this->sessionStub->method('getUser')->willReturn($user);
+        $this->sessionStub->method('getAction')->willReturn('join');
+
+        $userToJoin = $this->getFakeUser();
+        $userToJoin->id = '456';
+
+        $redirect = $this->loginHandler->getAfterLoginRedirect($userToJoin);
+
+        $this->assertEquals('http://rap-ia2/account', $redirect);
+    }
+
+    public function testJoinNewIdentity(): void {
+
+        $user = $this->getFakeUser();
+        $this->sessionStub->method('getUser')->willReturn($user);
+        $this->sessionStub->method('getAction')->willReturn('join');
+
+        $userToJoin = $this->getFakeUser();
+        // new identity
+        $userToJoin->id = null;
+
+        $redirect = $this->loginHandler->getAfterLoginRedirect($userToJoin);
+
+        $this->assertEquals('http://rap-ia2/account', $redirect);
+    }
+
+    public function testJoinAlreadyJoinedUser(): void { // go to account page without joining
+        $user = $this->getFakeUser();
+
+        $this->sessionStub->method('getAction')->willReturn('join');
+
+        $this->userDaoStub->method('findUserByIdentity')->willReturn($user);
+
+        $redirect = $this->loginHandler->onIdentityDataReceived('123', function($identity) {
+            $identity->email = 'test@example.com';
+        });
+
+        $this->assertEquals('http://rap-ia2/account', $redirect);
+    }
+
+    private function getFakeUser(): \RAP\User {
+
+        $user = new \RAP\User();
+        $user->id = '1';
+        $identity = new \RAP\Identity('eduGAIN');
+        $identity->email = 'test@example.com';
+        $identity->id = '4';
+        $identity->typedId = '123';
+        $user->addIdentity($identity);
+        return $user;
+    }
+
+}