Files
2026-07-19 12:02:41 -03:00

100 lines
3.3 KiB
Python

"""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