"""Contract tests for the in-memory directory that the other tickets build against.""" from datetime import UTC, datetime, timedelta import pytest from portal.adapters.directory import ( WRITABLE_ATTRIBUTES, DirectoryUnavailable, FakeDirectory, Forbidden, PasswordRejected, ValidationFailed, ) @pytest.fixture def directory(): d = FakeDirectory() d.add_user("Alice", given_name="Alice", mail="alice@example.test", is_publisher=True) d.add_user("gone", enabled=False) d.add_user("locked", locked=True) d.add_user("expired", password_last_set=None) return d def test_find_user_is_case_insensitive_and_keeps_the_exact_name(directory): assert directory.find_user("alice").sam == "Alice" assert directory.find_user("nobody") is None assert directory.find_user("") is None def test_account_state_is_reported(directory): assert directory.find_user("gone").enabled is False assert directory.find_user("locked").locked is True assert directory.find_user("expired").must_change_password is True assert directory.find_user("Alice").is_publisher is True assert directory.find_user("Alice").is_admin is False def test_verify_password(directory): assert directory.verify_password("alice", "Correct-Horse-9") is True assert directory.verify_password("alice", "wrong") is False assert directory.verify_password("nobody", "Correct-Horse-9") is False def test_an_empty_password_is_never_valid(directory): directory.add_user("blank", password="") assert directory.verify_password("blank", "") is False def test_allowed_attributes_can_be_written_and_cleared(directory): directory.update_attributes("alice", {"mobile": "+49 1", "givenName": "Al"}) user = directory.find_user("alice") assert (user.mobile, user.given_name) == ("+49 1", "Al") directory.update_attributes("alice", {"mobile": None}) assert directory.find_user("alice").mobile == "" @pytest.mark.parametrize( "name", ["description", "memberOf", "unicodePwd", "sAMAccountName", "userAccountControl"] ) def test_other_attributes_are_refused_before_anything_is_written(directory, name): with pytest.raises(Forbidden): directory.update_attributes("alice", {"givenName": "Changed", name: "x"}) assert directory.find_user("alice").given_name == "Alice" def test_writing_an_unknown_user_fails(directory): with pytest.raises(ValidationFailed): directory.update_attributes("nobody", {"mobile": "1"}) def test_writable_list_is_exactly_the_agreed_six(): assert set(WRITABLE_ATTRIBUTES) == { "givenName", "sn", "displayName", "mail", "telephoneNumber", "mobile", } def test_password_policy_is_returned(directory): policy = directory.password_policy("alice") assert policy.min_length == 8 assert policy.min_age == timedelta(0) def test_password_change_needs_the_current_password(directory): with pytest.raises(PasswordRejected) as exc: directory.change_password("alice", "wrong", "Another-Pass-7!") assert exc.value.reason == "wrong_current" directory.change_password("alice", "Correct-Horse-9", "Another-Pass-7!") assert directory.verify_password("alice", "Another-Pass-7!") is True assert directory.find_user("alice").password_last_set > datetime.now(UTC) - timedelta(minutes=1) @pytest.mark.parametrize("new", ["short1A", "alllowercase-long", "Correct-Horse-9"]) def test_policy_violations_are_reported_as_policy(directory, new): with pytest.raises(PasswordRejected) as exc: directory.change_password("alice", "Correct-Horse-9", new) assert exc.value.reason == "policy" def test_an_outage_is_a_typed_error(directory): directory.unavailable = True with pytest.raises(DirectoryUnavailable): directory.find_user("alice")