Files
volumen/tests/test_users.py
T

228 lines
8.4 KiB
Python
Raw Normal View History

"""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
2026-07-26 18:15:29 +02:00
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:
2026-07-26 18:15:29 +02:00
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:
2026-07-26 18:15:29 +02:00
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)
2026-07-26 18:15:29 +02:00
users.add("alice", "alice-test-password")
fresh = Users(path=users_path)
2026-07-26 18:15:29 +02:00
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