diff --git a/lib/GroupBackend.php b/lib/GroupBackend.php index e2a6d0b21..732fe6009 100644 --- a/lib/GroupBackend.php +++ b/lib/GroupBackend.php @@ -303,8 +303,8 @@ public function createGroup(string $name, ?string $samlGid = null): ?string { 'app' => 'user_saml', 'exception' => $e, ]); - return null; } + return null; } // Add to cache diff --git a/tests/unit/GroupBackendTest.php b/tests/unit/GroupBackendTest.php index 245687626..920c4020c 100644 --- a/tests/unit/GroupBackendTest.php +++ b/tests/unit/GroupBackendTest.php @@ -185,6 +185,91 @@ public function testSearchInGroupMatchesDisplayNameAndEmail(): void { $this->assertArrayHasKey($this->users[0]['uid'], $byUid, 'UID search should still work'); } + public function testGetBackendName(): void { + $this->assertSame('user_saml', $this->groupBackend->getBackendName()); + } + + public function testGetDisplayName(): void { + $group = $this->groups[0]; + $this->assertSame($group['saml_gid'], $this->groupBackend->getDisplayName($group['gid'])); + + // falls back to the gid itself when the group is unknown + $this->assertSame('unknown_gid', $this->groupBackend->getDisplayName('unknown_gid')); + } + + public function testSetDisplayName(): void { + $group = $this->groups[2]; + $this->assertTrue($this->groupBackend->setDisplayName($group['gid'], 'New Display Name')); + $this->assertSame('New Display Name', $this->groupBackend->getDisplayName($group['gid'])); + + $this->assertFalse($this->groupBackend->setDisplayName('unknown_gid', 'New Display Name')); + } + + public function testGroupsExists(): void { + $gids = array_column($this->groups, 'gid'); + $result = $this->groupBackend->groupsExists([...$gids, 'unknown_gid']); + + $this->assertCount(count($gids), $result); + foreach ($gids as $gid) { + $this->assertContains($gid, $result); + } + } + + public function testGetGroupDetails(): void { + $group = $this->groups[0]; + $this->assertSame(['displayName' => $group['saml_gid']], $this->groupBackend->getGroupDetails($group['gid'])); + $this->assertSame([], $this->groupBackend->getGroupDetails('unknown_gid')); + } + + public function testGetGroupsDetails(): void { + $group = $this->groups[0]; + $result = $this->groupBackend->getGroupsDetails([$group['gid'], 'unknown_gid']); + + $this->assertSame(['displayName' => $group['saml_gid']], $result[$group['gid']]); + $this->assertArrayNotHasKey('unknown_gid', $result); + } + + public function testAddToGroupIsIdempotent(): void { + $group = $this->groups[2]; + $uid = $this->users[0]['uid']; + $this->assertFalse($this->groupBackend->inGroup($uid, $group['gid'])); + + $this->assertTrue($this->groupBackend->addToGroup($uid, $group['gid'])); + $this->assertTrue($this->groupBackend->inGroup($uid, $group['gid'])); + + // adding an already-member user is a no-op that still reports success + $this->assertTrue($this->groupBackend->addToGroup($uid, $group['gid'])); + + $this->groupBackend->removeFromGroup($uid, $group['gid']); + } + + public function testRemoveFromGroupReturnsFalseWhenNotMember(): void { + $group = $this->groups[2]; + $uid = $this->users[0]['uid']; + + $this->assertFalse($this->groupBackend->removeFromGroup($uid, $group['gid'])); + + $this->groupBackend->addToGroup($uid, $group['gid']); + $this->assertTrue($this->groupBackend->removeFromGroup($uid, $group['gid'])); + } + + public function testCreateGroupReturnsNullOnDuplicate(): void { + $existingGroup = $this->groups[0]; + $this->assertNull($this->groupBackend->createGroup($existingGroup['gid'])); + } + + public function testDeleteGroup(): void { + $gid = $this->groupBackend->createGroup('user_saml_integration_test_throwaway_group'); + $this->assertNotNull($gid); + $this->assertTrue($this->groupBackend->groupExists($gid)); + + $this->assertTrue($this->groupBackend->deleteGroup($gid)); + $this->assertFalse($this->groupBackend->groupExists($gid)); + + // deleting an already-deleted group still reports success + $this->assertTrue($this->groupBackend->deleteGroup($gid)); + } + private function resetAccountData(): void { foreach ($this->users as $user) { $qb = $this->connection->getQueryBuilder(); diff --git a/tests/unit/UserBackendIntegrationTest.php b/tests/unit/UserBackendIntegrationTest.php new file mode 100644 index 000000000..f5a4355de --- /dev/null +++ b/tests/unit/UserBackendIntegrationTest.php @@ -0,0 +1,162 @@ + 'user_saml_integration_test_uid1', + 'displayname' => 'SAML Integration User One', + 'home' => '/tmp/user_saml_integration_test_uid1', + ], + [ + 'uid' => 'user_saml_integration_test_uid2', + 'displayname' => 'SAML Integration User Two', + 'home' => '/tmp/user_saml_integration_test_uid2', + ], + ]; + + #[\Override] + protected function setUp(): void { + parent::setUp(); + + $this->db = \OCP\Server::get(IDBConnection::class); + $this->cleanupUsers(); + $this->cleanupKnownUsers(); + + foreach ($this->users as $user) { + $qb = $this->db->getQueryBuilder(); + $qb->insert('user_saml_users') + ->setValue('uid', $qb->createNamedParameter($user['uid'])) + ->setValue('displayname', $qb->createNamedParameter($user['displayname'])) + ->setValue('home', $qb->createNamedParameter($user['home'])) + ->executeStatement(); + } + + $this->userBackend = new UserBackend( + $this->createMock(IConfig::class), + $this->createMock(IAppConfig::class), + $this->createMock(IURLGenerator::class), + $this->createMock(ISession::class), + $this->db, + $this->createMock(IUserManager::class), + $this->createMock(GroupManager::class), + $this->getMockBuilder(SAMLSettings::class)->disableOriginalConstructor()->getMock(), + $this->createMock(LoggerInterface::class), + $this->createMock(UserData::class), + $this->createMock(IEventDispatcher::class), + 'serverRoot', + ); + } + + #[\Override] + protected function tearDown(): void { + parent::tearDown(); + $this->cleanupUsers(); + $this->cleanupKnownUsers(); + } + + private function cleanupUsers(): void { + foreach ($this->users as $user) { + $qb = $this->db->getQueryBuilder(); + $qb->delete('user_saml_users') + ->where($qb->expr()->eq('uid', $qb->createNamedParameter($user['uid']))) + ->executeStatement(); + } + } + + private function cleanupKnownUsers(): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('known_users') + ->where($qb->expr()->eq('known_to', $qb->createNamedParameter('user_saml_integration_test_searcher'))) + ->executeStatement(); + } + + public function testUserExists(): void { + $this->assertTrue($this->userBackend->userExists('user_saml_integration_test_uid1')); + $this->assertFalse($this->userBackend->userExists('user_saml_integration_test_nonexistent')); + } + + public function testGetHome(): void { + $this->assertSame('/tmp/user_saml_integration_test_uid1', $this->userBackend->getHome('user_saml_integration_test_uid1')); + $this->assertFalse($this->userBackend->getHome('user_saml_integration_test_nonexistent')); + } + + public function testGetDisplayName(): void { + $this->assertSame('SAML Integration User One', $this->userBackend->getDisplayName('user_saml_integration_test_uid1')); + // falls back to the uid itself when no row exists + $this->assertSame('user_saml_integration_test_nonexistent', $this->userBackend->getDisplayName('user_saml_integration_test_nonexistent')); + } + + public function testGetDisplayNames(): void { + $displayNames = $this->userBackend->getDisplayNames('SAML Integration User'); + $this->assertSame('SAML Integration User One', $displayNames['user_saml_integration_test_uid1']); + $this->assertSame('SAML Integration User Two', $displayNames['user_saml_integration_test_uid2']); + } + + public function testGetUsers(): void { + $users = $this->userBackend->getUsers('user_saml_integration_test'); + $this->assertContains('user_saml_integration_test_uid1', $users); + $this->assertContains('user_saml_integration_test_uid2', $users); + } + + public function testSetDisplayName(): void { + $this->assertTrue($this->userBackend->setDisplayName('user_saml_integration_test_uid1', 'Renamed User')); + $this->assertSame('Renamed User', $this->userBackend->getDisplayName('user_saml_integration_test_uid1')); + + $this->assertFalse($this->userBackend->setDisplayName('user_saml_integration_test_nonexistent', 'Nope')); + } + + public function testCountUsers(): void { + $this->assertSame(count($this->users), $this->userBackend->countUsers()); + } + + public function testDeleteUser(): void { + $this->assertTrue($this->userBackend->deleteUser('user_saml_integration_test_uid1')); + $this->assertFalse($this->userBackend->userExists('user_saml_integration_test_uid1')); + + // deleting an already-deleted (or never existing) user reports no rows affected + $this->assertFalse($this->userBackend->deleteUser('user_saml_integration_test_uid1')); + } + + public function testSearchKnownUsersByDisplayName(): void { + $qb = $this->db->getQueryBuilder(); + $qb->insert('known_users') + ->setValue('known_to', $qb->createNamedParameter('user_saml_integration_test_searcher')) + ->setValue('known_user', $qb->createNamedParameter('user_saml_integration_test_uid1')) + ->executeStatement(); + + $result = $this->userBackend->searchKnownUsersByDisplayName('user_saml_integration_test_searcher', 'User One'); + $this->assertSame(['user_saml_integration_test_uid1' => 'SAML Integration User One'], $result); + + $result = $this->userBackend->searchKnownUsersByDisplayName('user_saml_integration_test_searcher', 'User Two'); + $this->assertSame([], $result); + } +} diff --git a/tests/unit/UserBackendTest.php b/tests/unit/UserBackendTest.php index e91a37c8f..fa8cdb241 100644 --- a/tests/unit/UserBackendTest.php +++ b/tests/unit/UserBackendTest.php @@ -9,7 +9,10 @@ namespace OCA\User_SAML\Tests\Settings; +use OC\Security\CSRF\CsrfToken; +use OC\Security\CSRF\CsrfTokenManager; use OCA\User_SAML\GroupManager; +use OCA\User_SAML\Model\SessionData; use OCA\User_SAML\SAMLSettings; use OCA\User_SAML\UserBackend; use OCA\User_SAML\UserData; @@ -21,7 +24,9 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; +use OCP\IUserSession; use OCP\User\Backend\IProvideEnabledStateBackend; +use OCP\User\Backend\ISetDisplayNameBackend; use OCP\User\Events\UserChangedEvent; use OCP\UserInterface; use Override; @@ -32,6 +37,9 @@ interface EnabledStateUserInterface extends UserInterface, IProvideEnabledStateBackend { } +interface DisplayNameUserInterface extends UserInterface, ISetDisplayNameBackend { +} + class UserBackendTest extends TestCase { private UserData&MockObject $userData; private IConfig&MockObject $config; @@ -360,4 +368,160 @@ public function testUpdateAttributesQuotaDefaultFallback(): void { ->willReturn([]); $this->userBackend->updateAttributes('ExistingUser'); } + + public function testHasUserListingsReflectsAutoprovisioning(): void { + $this->userBackend = $this->getRealUserBackend(); + + $this->appConfig->method('getAppValueInt') + ->with('general-require_provisioned_account') + ->willReturn(0); + $this->assertTrue($this->userBackend->hasUserListings()); + } + + public function testHasUserListingsFalseWhenProvisionedAccountRequired(): void { + $this->userBackend = $this->getRealUserBackend(); + + $this->appConfig->method('getAppValueInt') + ->with('general-require_provisioned_account') + ->willReturn(1); + $this->assertFalse($this->userBackend->hasUserListings()); + } + + public function testIsSessionActiveWithIdentityProvider(): void { + $this->userBackend = $this->getRealUserBackend(); + + $this->session->method('get') + ->with(SessionData::KEY_IDENTITY_PROVIDER_ID) + ->willReturn(1); + $this->assertTrue($this->userBackend->isSessionActive()); + } + + public function testIsSessionActiveWithoutIdentityProvider(): void { + $this->userBackend = $this->getRealUserBackend(); + + $this->session->method('get') + ->with(SessionData::KEY_IDENTITY_PROVIDER_ID) + ->willReturn(null); + $this->assertFalse($this->userBackend->isSessionActive()); + } + + public function testGetLogoutUrlWithoutSingleLogoutService(): void { + $this->userBackend = $this->getRealUserBackend(); + $token = $this->createMock(CsrfToken::class); + $tokenManager = $this->createMock(CsrfTokenManager::class); + $this->overwriteService(CsrfTokenManager::class, $tokenManager); + + $this->SAMLSettings->method('getProviderId')->willReturn(1); + $this->SAMLSettings->method('get')->with(1)->willReturn([]); + $token->method('getEncryptedValue')->willReturn('token-value'); + $tokenManager->method('getToken')->willReturn($token); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with('core.login.logout', ['requesttoken' => 'token-value']) + ->willReturn('https://example.com/logout'); + + $this->assertSame('https://example.com/logout', $this->userBackend->getLogoutUrl()); + } + + public function testGetLogoutUrlWithSingleLogoutService(): void { + $this->userBackend = $this->getRealUserBackend(); + $token = $this->createMock(CsrfToken::class); + $tokenManager = $this->createMock(CsrfTokenManager::class); + $this->overwriteService(CsrfTokenManager::class, $tokenManager); + + $this->SAMLSettings->method('getProviderId')->willReturn(1); + $this->SAMLSettings->method('get')->with(1)->willReturn(['idp-singleLogoutService.url' => 'https://idp.example.com/slo']); + $token->method('getEncryptedValue')->willReturn('token-value'); + $tokenManager->method('getToken')->willReturn($token); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with('user_saml.SAML.singleLogoutService', ['requesttoken' => 'token-value']) + ->willReturn('https://example.com/slo'); + + $this->assertSame('https://example.com/slo', $this->userBackend->getLogoutUrl()); + } + + public function testGetCurrentUserIdFromActiveSession(): void { + $this->userBackend = $this->getMockedBuilder(['userExists']); + $userSession = $this->createMock(IUserSession::class); + $this->overwriteService(IUserSession::class, $userSession); + $user = $this->createMock(IUser::class); + + $user->method('getUID')->willReturn('ExistingUser'); + $userSession->method('getUser')->willReturn($user); + $this->session->method('get') + ->with('user_saml.samlUserData') + ->willReturn(['attribute' => 'value']); + $this->userBackend->method('userExists')->with('ExistingUser')->willReturn(true); + + $this->assertSame('ExistingUser', $this->userBackend->getCurrentUserId()); + } + + public function testGetCurrentUserIdReturnsEmptyWhenUserDoesNotExist(): void { + $this->userBackend = $this->getMockedBuilder(['userExists']); + $userSession = $this->createMock(IUserSession::class); + $this->overwriteService(IUserSession::class, $userSession); + + $userSession->method('getUser')->willReturn(null); + $this->session->method('get')->willReturn(null); + $this->userData->method('getEffectiveUid')->willReturn(''); + $this->userBackend->method('userExists')->willReturn(false); + + $this->assertSame('', $this->userBackend->getCurrentUserId()); + } + + public function testUserExistsDelegatesToActualBackend(): void { + $this->userBackend = $this->getMockedBuilder(['getActualUserBackend']); + $backend = $this->createMock(UserInterface::class); + + $this->userBackend + ->expects($this->once()) + ->method('getActualUserBackend') + ->with('ExistingUser') + ->willReturn($backend); + $backend->expects($this->once()) + ->method('userExists') + ->with('ExistingUser') + ->willReturn(true); + + $this->assertTrue($this->userBackend->userExists('ExistingUser')); + } + + public function testGetDisplayNameDelegatesToActualBackend(): void { + $this->userBackend = $this->getMockedBuilder(['getActualUserBackend']); + /** @var DisplayNameUserInterface&MockObject $backend */ + $backend = $this->createMock(DisplayNameUserInterface::class); + + $this->userBackend + ->expects($this->once()) + ->method('getActualUserBackend') + ->with('ExistingUser') + ->willReturn($backend); + $backend->expects($this->once()) + ->method('getDisplayName') + ->with('ExistingUser') + ->willReturn('Actual Backend Name'); + + $this->assertSame('Actual Backend Name', $this->userBackend->getDisplayName('ExistingUser')); + } + + public function testSetDisplayNameDelegatesToActualBackend(): void { + $this->userBackend = $this->getMockedBuilder(['getActualUserBackend']); + /** @var DisplayNameUserInterface&MockObject $backend */ + $backend = $this->createMock(DisplayNameUserInterface::class); + + $this->userBackend + ->expects($this->once()) + ->method('getActualUserBackend') + ->with('ExistingUser') + ->willReturn($backend); + $backend->expects($this->once()) + ->method('setDisplayName') + ->with('ExistingUser', 'New Name') + ->willReturn(true); + + $this->assertTrue($this->userBackend->setDisplayName('ExistingUser', 'New Name')); + } }