"""Tests for Users — file-backed admin/author management.""" from __future__ import annotations import os from volumen.users import Users class TestBootstrap: def test_bootstrap_user_from_hash(self, users_path: str, bootstrap_hash: str) -> None: users = Users(path=users_path, bootstrap_hash=bootstrap_hash) assert len(users.all) == 1 assert users.authenticate("admin", "volumen-test-password") is not None def test_bootstrap_user_is_admin(self, users_path: str, bootstrap_hash: str) -> None: users = Users(path=users_path, bootstrap_hash=bootstrap_hash) admin = users.find("admin") assert admin is not None assert admin.role == "admin" def test_no_users_without_file_or_hash(self, users_path: str) -> None: users = Users(path=users_path) assert not users.any class TestFind: def test_find_existing_user(self, users: Users) -> None: assert users.find("admin") is not None def test_find_nonexistent_user(self, users: Users) -> None: assert users.find("nobody") is None class TestAuthenticate: def test_authenticate_correct_password(self, users: Users) -> None: assert users.authenticate("admin", "volumen-test-password") is not None def test_authenticate_wrong_password(self, users: Users) -> None: assert users.authenticate("admin", "nope") is None def test_authenticate_nonexistent_user(self, users: Users) -> None: assert users.authenticate("nobody", "volumen-test-password") is None class TestAdd: def test_add_creates_user(self, users_path: str) -> None: users = Users(path=users_path) result = users.add("alice", "wonderland") assert result is not None assert result.username == "alice" assert os.path.isfile(users_path) def test_add_and_authenticate(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "wonderland") assert users.authenticate("alice", "wonderland") is not None assert users.authenticate("alice", "nope") is None def test_add_rejects_duplicate(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") assert users.add("alice", "y") is None def test_add_rejects_empty_username(self, users_path: str) -> None: users = Users(path=users_path) assert users.add("", "password") is None def test_add_with_explicit_role(self, users_path: str) -> None: users = Users(path=users_path) users.add("boss", "x", "admin") assert users.find("boss").role == "admin" def test_add_default_role_is_author(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") assert users.find("alice").role == "author" def test_add_invalid_role_falls_back_to_default(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x", "wizard") assert users.find("alice").role == "author" class TestUpdate: def test_update_name(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") users.update_name("alice", "Alice Smith") assert users.find("alice").name == "Alice Smith" def test_update_name_clear(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") users.update_name("alice", "Alice") users.update_name("alice", None) assert users.find("alice").name is None def test_update_fediverse_creator(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") users.update_fediverse_creator("alice", "@alice@example.com") assert users.find("alice").fediverse_creator == "@alice@example.com" def test_update_fediverse_creator_clear(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") users.update_fediverse_creator("alice", "@alice@example.com") users.update_fediverse_creator("alice", None) assert users.find("alice").fediverse_creator is None def test_update_photo(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") users.update_photo("alice", "/media/photo.webp") assert users.find("alice").photo == "/media/photo.webp" def test_update_password(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "old") users.update_password("alice", "new") assert users.authenticate("alice", "old") is None assert users.authenticate("alice", "new") is not None class TestRename: def test_rename_user(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") assert users.rename("alice", "alicia") is not None assert users.find("alice") is None assert users.find("alicia") is not None def test_rename_to_existing_username(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") users.add("bob", "y") assert users.rename("alice", "bob") is None assert users.find("alice") is not None def test_rename_empty_new_name(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") assert users.rename("alice", "") is None class TestSetRole: def test_set_role(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x", "admin") users.add("bob", "y", "author") assert users.set_role("bob", "admin") is not None assert users.find("bob").role == "admin" def test_cannot_demote_last_admin(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x", "admin") users.add("bob", "y", "author") assert users.set_role("alice", "author") is None assert users.find("alice").role == "admin" def test_set_role_invalid(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") assert users.set_role("alice", "wizard") is None class TestDelete: def test_delete_user(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") users.add("bob", "y") assert users.delete("bob") == "bob" assert users.find("bob") is None def test_cannot_delete_last_user(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") assert users.delete("alice") is None assert users.find("alice") is not None def test_cannot_delete_last_admin(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x", "admin") users.add("bob", "y", "author") assert users.delete("alice") is None # After adding another admin, deletion works users.add("carol", "z", "admin") assert users.delete("alice") == "alice" class TestPersistence: def test_persistence_write_and_read(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "wonderland") users.add("bob", "builder", "admin") users.update_name("alice", "Alice") # Read back from disk with a fresh instance fresh = Users(path=users_path) assert len(fresh.all) == 2 alice = fresh.find("alice") assert alice is not None assert alice.name == "Alice" assert alice.role == "author" bob = fresh.find("bob") assert bob is not None assert bob.role == "admin" def test_password_survives_persistence(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "alice-test-password") fresh = Users(path=users_path) assert fresh.authenticate("alice", "alice-test-password") is not None assert fresh.authenticate("alice", "wrong") is None def test_mtime_cache_invalidation(self, users_path: str) -> None: users = Users(path=users_path) users.add("alice", "x") assert len(users.all) == 1 # Directly modify the file behind the cache with open(users_path, "w") as fh: fh.write("") # Should detect file change and return empty assert len(users.all) == 0