"""LDAP authentication tests. The blocking directory bind (``_authenticate_sync``) is replaced with a fake so no real LDAP server is needed; everything above it — the enabled gate, the threadpool hop, JIT provisioning, and local fallback — runs for real. """ import pytest from app.core.config import settings from app.crud import user as user_crud from app.services import ldap as ldap_service DIRECTORY = { # login name -> (password, email, full_name) "jdoe": ("ldap-pass", "jdoe@corp.example", "Jane Doe"), } def _fake_sync(username: str, password: str): record = DIRECTORY.get(username) if record is None or record[0] != password: return None _, email, full_name = record return ldap_service.LdapUser(email=email.lower(), full_name=full_name) @pytest.fixture def ldap_enabled(monkeypatch): monkeypatch.setattr(settings, "ldap_enabled", True) monkeypatch.setattr(settings, "ldap_default_roles", ["user"]) monkeypatch.setattr(ldap_service, "_authenticate_sync", _fake_sync) async def test_ldap_login_provisions_local_user( client, seeded, db_sessionmaker, ldap_enabled ): resp = await client.post( "/api/auth/login", data={"username": "jdoe", "password": "ldap-pass"}, ) assert resp.status_code == 200, resp.text access = resp.json()["access_token"] # A local row was created from the directory entry. async with db_sessionmaker() as db: user = await user_crud.get_by_email(db, "jdoe@corp.example") assert user is not None assert user.auth_provider == "ldap" assert user.full_name == "Jane Doe" assert "user" in {r.name for r in user.roles} # default role applied # The issued token authenticates against the API. me = await client.get( "/api/users/me", headers={"Authorization": f"Bearer {access}"} ) assert me.status_code == 200 assert me.json()["email"] == "jdoe@corp.example" assert me.json()["auth_provider"] == "ldap" async def test_ldap_wrong_password_rejected(client, seeded, ldap_enabled): resp = await client.post( "/api/auth/login", data={"username": "jdoe", "password": "nope"}, ) assert resp.status_code == 401 async def test_ldap_user_cannot_use_local_password( client, seeded, db_sessionmaker, ldap_enabled ): # Provision via LDAP first. await client.post( "/api/auth/login", data={"username": "jdoe", "password": "ldap-pass"} ) # The local password slot is unusable, so DB auth must fail for this user # even though the account now exists. async with db_sessionmaker() as db: assert ( await user_crud.authenticate(db, "jdoe@corp.example", "ldap-pass") is None ) async def test_local_user_still_logs_in_when_ldap_enabled( client, seeded, make_user, ldap_enabled ): # A local account that does NOT exist in the directory falls back to DB auth. await make_user("local@example.com", password="secret123") resp = await client.post( "/api/auth/login", data={"username": "local@example.com", "password": "secret123"}, ) assert resp.status_code == 200, resp.text async def test_empty_password_is_rejected(ldap_enabled): # Guards against an anonymous bind masquerading as a successful login. assert await ldap_service.authenticate_ldap("jdoe", "") is None