"""Unit tests for LDAP user-bind authentication (ldap3 fully mocked).""" from unittest.mock import MagicMock import pytest from app.core.config import Settings from app.services.ldap_auth import LdapAuthError, authenticate def _settings(**overrides: object) -> Settings: base: dict[str, object] = { "ldap_enabled": True, "ldap_uri": "ldaps://ads-server-01.insight.local:636", "ldap_domain": "insight.local", "ldap_user_search_base": "DC=insight,DC=local", "ldap_user_filter": "(sAMAccountName={username})", "ldap_admin_group": "CN=Domain Admins,CN=Users,DC=insight,DC=local", "ldap_timeout_seconds": 10, } base.update(overrides) return Settings(**base) # type: ignore[arg-type] def _entry(full_name: str = "Flo Hartmann", email: str = "flo@example.com") -> MagicMock: entry = MagicMock() entry.displayName = full_name entry.mail = email return entry def _patch_ldap(monkeypatch: pytest.MonkeyPatch, conn: MagicMock) -> None: mock_ldap3 = MagicMock() mock_ldap3.Server.return_value = MagicMock() mock_ldap3.Connection.return_value = conn mock_ldap3.SUBTREE = "SUBTREE" mock_ldap3.NONE = "NO_INFO" mock_ldap3.utils.conv.escape_filter_chars = lambda s: s monkeypatch.setattr("app.services.ldap_auth.ldap3", mock_ldap3) def test_disabled_raises() -> None: with pytest.raises(LdapAuthError, match="nicht aktiviert"): authenticate("flo", "secret", _settings(ldap_enabled=False)) def test_missing_admin_group_raises() -> None: with pytest.raises(LdapAuthError, match="nicht konfiguriert"): authenticate("flo", "secret", _settings(ldap_admin_group="")) def test_empty_password_raises() -> None: with pytest.raises(LdapAuthError): authenticate("flo", "", _settings()) def test_bind_failure_raises_generic(monkeypatch: pytest.MonkeyPatch) -> None: from ldap3.core.exceptions import LDAPExceptionError mock_ldap3 = MagicMock() mock_ldap3.Server.return_value = MagicMock() mock_ldap3.Connection.side_effect = LDAPExceptionError("invalidCredentials") monkeypatch.setattr("app.services.ldap_auth.ldap3", mock_ldap3) with pytest.raises(LdapAuthError, match="Benutzername oder Passwort falsch"): authenticate("flo", "wrong", _settings()) def test_search_without_match_denied(monkeypatch: pytest.MonkeyPatch) -> None: conn = MagicMock() conn.__enter__.return_value = conn conn.search.return_value = True conn.entries = [] _patch_ldap(monkeypatch, conn) with pytest.raises(LdapAuthError, match="Kein Zugriff"): authenticate("flo", "secret", _settings()) def test_success_returns_admin_user(monkeypatch: pytest.MonkeyPatch) -> None: conn = MagicMock() conn.__enter__.return_value = conn conn.search.return_value = True conn.entries = [_entry()] mock_ldap3 = MagicMock() mock_ldap3.Server.return_value = MagicMock() mock_ldap3.Connection.return_value = conn mock_ldap3.SUBTREE = "SUBTREE" mock_ldap3.NONE = "NO_INFO" mock_ldap3.utils.conv.escape_filter_chars = lambda s: s monkeypatch.setattr("app.services.ldap_auth.ldap3", mock_ldap3) user = authenticate("flo", "secret", _settings()) assert user.username == "flo" assert user.full_name == "Flo Hartmann" assert user.email == "flo@example.com" assert user.is_admin is True # bind DN uses user@domain format assert mock_ldap3.Connection.call_args.kwargs["user"] == "flo@insight.local" # search filter proves admin-group membership via memberOf search_kwargs = conn.search.call_args.kwargs assert "(memberOf=CN=Domain Admins,CN=Users,DC=insight,DC=local)" in search_kwargs["search_filter"] assert search_kwargs["search_base"] == "DC=insight,DC=local" def test_optional_fields_none_when_empty(monkeypatch: pytest.MonkeyPatch) -> None: conn = MagicMock() conn.__enter__.return_value = conn conn.search.return_value = True conn.entries = [_entry(full_name="", email="")] mock_ldap3 = MagicMock() mock_ldap3.Server.return_value = MagicMock() mock_ldap3.Connection.return_value = conn mock_ldap3.SUBTREE = "SUBTREE" mock_ldap3.NONE = "NO_INFO" mock_ldap3.utils.conv.escape_filter_chars = lambda s: s monkeypatch.setattr("app.services.ldap_auth.ldap3", mock_ldap3) user = authenticate("flo", "secret", _settings()) assert user.full_name is None assert user.email is None