import pytest from typing import List, Optional from app.core.errors import ApiError, map_command_error from app.core.models import UserCreateRequest import app.providers.cli_provider as cli_provider from app.providers.cli_provider import CliSystemProvider from app.providers.base import SystemProvider from app.services.user_group_service import UserGroupService class NoopProvider(SystemProvider): def __init__(self) -> None: self.created_home_dir: Optional[str] = None self.created_linked_home_dir: Optional[str] = None def create_user(self, username: str, password_hash: str, home_dir: Optional[str], linked_home_dir: Optional[str], shell: str, primary_group: Optional[str], groups: List[str]) -> None: self.created_home_dir = home_dir self.created_linked_home_dir = linked_home_dir return None def delete_user(self, username: str) -> None: return None def change_user_password(self, username: str, password_hash: str) -> None: return None def list_users(self): return [] def get_user(self, username: str): raise ApiError(404, "not_found", "not found") def create_group(self, groupname: str) -> None: return None def delete_group(self, groupname: str) -> None: return None def list_groups(self): return [] def get_group(self, groupname: str): raise ApiError(404, "not_found", "not found") def add_user_groups(self, username: str, groups: List[str], replace: bool) -> None: return None def remove_user_groups(self, username: str, groups: List[str]) -> None: return None def get_user_groups(self, username: str): return [] def test_home_dir_out_of_base_rejected() -> None: service = UserGroupService(provider=NoopProvider(), home_base_dir="/home") payload = UserCreateRequest(username="alice", password_hash="$6$rounds=5000$abcdefghij", home_dir="/tmp/alice", groups=[]) with pytest.raises(ApiError) as raised: service.create_user(payload) assert raised.value.status_code == 400 def test_link_home_dir_uses_home_base_symlink_path_and_external_storage() -> None: provider = NoopProvider() service = UserGroupService(provider=provider, home_base_dir="/home", link_home_dir="/data/home") payload = UserCreateRequest(username="alice", password_hash="$6$rounds=5000$abcdefghij", groups=[]) service.create_user(payload) assert provider.created_home_dir == "/home/alice" assert provider.created_linked_home_dir == "/data/home/alice" def test_command_error_mapping_conflict() -> None: error = map_command_error("user already exists", 9) assert error.status_code == 409 class RecordingExecutor: def __init__(self, home_dir: str) -> None: self.home_dir = home_dir self.commands: List[List[str]] = [] def run(self, args: List[str], use_sudo: bool = True) -> str: self.commands.append(args) if args == ["getent", "passwd", "alice"]: return f"alice:x:1000:1000::%s:/bin/bash" % self.home_dir return "" class FakePath: def __init__(self, path: str) -> None: self.path = path def is_symlink(self) -> bool: return self.path == "/home/alice" def test_delete_user_unlinks_home_when_it_is_symlink(monkeypatch) -> None: monkeypatch.setattr(cli_provider, "Path", FakePath) executor = RecordingExecutor("/home/alice") provider = CliSystemProvider(executor=executor) provider.delete_user("alice") assert ["userdel", "alice"] in executor.commands assert ["unlink", "/home/alice"] in executor.commands def test_delete_user_does_not_unlink_home_when_it_is_directory(monkeypatch) -> None: monkeypatch.setattr(cli_provider, "Path", FakePath) executor = RecordingExecutor("/home/old-alice") provider = CliSystemProvider(executor=executor) provider.delete_user("alice") assert ["userdel", "alice"] in executor.commands assert ["unlink", "/home/old-alice"] not in executor.commands