976398d1f2
- Introduce 'astrbot bk' command with GPG signing, encryption, and digest support - Add import/export functionality using core backup modules - Refactor path management to use 'AstrbotPaths' singleton across CLI commands - Replace blocking subprocess calls with asyncio.create_subprocess_exec in backup command - Add comprehensive tests for uninstall and backup commands - Improve module resource handling for bundled dashboard assets
139 lines
4.4 KiB
Python
139 lines
4.4 KiB
Python
import platform
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, call, patch
|
|
|
|
import pytest
|
|
from click.testing import CliRunner
|
|
|
|
from astrbot.cli.commands.cmd_uninstall import uninstall
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_systemctl():
|
|
"""Mock shutil.which('systemctl') and subprocess.run"""
|
|
with patch("astrbot.cli.commands.cmd_uninstall.shutil.which") as mock_which, patch(
|
|
"astrbot.cli.commands.cmd_uninstall.subprocess.run"
|
|
) as mock_run:
|
|
mock_which.return_value = "/usr/bin/systemctl"
|
|
yield mock_which, mock_run
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_astrbot_paths(tmp_path):
|
|
"""Mock astrbot_paths to use a temporary directory"""
|
|
with patch("astrbot.cli.commands.cmd_uninstall.astrbot_paths") as mock_paths:
|
|
# Create a fake astrbot root structure in tmp_path
|
|
root = tmp_path / "astrbot_root"
|
|
root.mkdir()
|
|
data = root / "data"
|
|
data.mkdir()
|
|
dot_astrbot = root / ".astrbot"
|
|
dot_astrbot.touch()
|
|
lock_file = root / "astrbot.lock"
|
|
lock_file.touch()
|
|
|
|
mock_paths.root = root
|
|
mock_paths.data = data
|
|
yield mock_paths
|
|
|
|
|
|
@pytest.mark.skipif(platform.system() != "Linux", reason="Systemd tests only on Linux")
|
|
def test_uninstall_systemd_service(mock_systemctl, mock_astrbot_paths):
|
|
"""Test systemd service removal"""
|
|
mock_which, mock_run = mock_systemctl
|
|
runner = CliRunner()
|
|
|
|
# Mock Path.home() to return a temp directory
|
|
with patch("pathlib.Path.home") as mock_home:
|
|
fake_home = mock_astrbot_paths.root / "home"
|
|
fake_home.mkdir()
|
|
mock_home.return_value = fake_home
|
|
|
|
# Create fake service file
|
|
service_dir = fake_home / ".config" / "systemd" / "user"
|
|
service_dir.mkdir(parents=True)
|
|
service_file = service_dir / "astrbot.service"
|
|
service_file.write_text("fake service content")
|
|
|
|
# Run with --yes to skip confirmation, --keep-data to focus on systemd
|
|
result = runner.invoke(uninstall, ["--yes", "--keep-data"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Stopping AstrBot service..." in result.output
|
|
assert "Systemd service uninstalled." in result.output
|
|
|
|
# Verify subprocess calls
|
|
mock_run.assert_any_call(
|
|
["systemctl", "--user", "stop", "astrbot"], check=False
|
|
)
|
|
mock_run.assert_any_call(
|
|
["systemctl", "--user", "disable", "astrbot"], check=False
|
|
)
|
|
mock_run.assert_any_call(["systemctl", "--user", "daemon-reload"], check=True)
|
|
|
|
# Verify file removed
|
|
assert not service_file.exists()
|
|
|
|
|
|
def test_uninstall_data_removal(mock_astrbot_paths):
|
|
"""Test data directory removal"""
|
|
runner = CliRunner()
|
|
|
|
# Verify pre-state
|
|
assert mock_astrbot_paths.data.exists()
|
|
assert (mock_astrbot_paths.root / ".astrbot").exists()
|
|
assert (mock_astrbot_paths.root / "astrbot.lock").exists()
|
|
|
|
# Run uninstall with --yes
|
|
result = runner.invoke(uninstall, ["--yes"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "AstrBot data removed successfully" in result.output
|
|
|
|
# Verify removal
|
|
assert not mock_astrbot_paths.data.exists()
|
|
assert not (mock_astrbot_paths.root / ".astrbot").exists()
|
|
assert not (mock_astrbot_paths.root / "astrbot.lock").exists()
|
|
|
|
|
|
def test_uninstall_keep_data(mock_astrbot_paths):
|
|
"""Test uninstall with --keep-data"""
|
|
runner = CliRunner()
|
|
|
|
result = runner.invoke(uninstall, ["--yes", "--keep-data"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "Skipping data removal as requested" in result.output
|
|
|
|
# Verify data still exists
|
|
assert mock_astrbot_paths.data.exists()
|
|
|
|
|
|
def test_uninstall_abort_on_no_confirm(mock_astrbot_paths):
|
|
"""Test abort when user declines confirmation"""
|
|
runner = CliRunner()
|
|
|
|
# Input "n" to decline
|
|
result = runner.invoke(uninstall, input="n\n")
|
|
|
|
assert result.exit_code != 0
|
|
assert "Aborted" in result.output
|
|
|
|
# Verify data still exists
|
|
assert mock_astrbot_paths.data.exists()
|
|
|
|
|
|
def test_uninstall_not_astrbot_root(mock_astrbot_paths):
|
|
"""Test running uninstall in a non-AstrBot directory"""
|
|
# Remove the marker files
|
|
shutil.rmtree(mock_astrbot_paths.data)
|
|
(mock_astrbot_paths.root / ".astrbot").unlink()
|
|
|
|
runner = CliRunner()
|
|
result = runner.invoke(uninstall, ["--yes"])
|
|
|
|
assert result.exit_code == 0
|
|
assert "No AstrBot initialization found" in result.output
|