test(cli): ✅ add tests for prune command

add tests for prune command and some code optimization and formatting.
This commit is contained in:
2026-09-25 17:59:27 +02:00
parent 48503cdd42
commit e50797bba6
4 changed files with 91 additions and 22 deletions
+11
View File
@@ -34,6 +34,17 @@ def tmp_setup_logging(tmp_log_file):
setup_logging(file_path=tmp_log_file)
@pytest.fixture(autouse=True)
def tmp_cli_logging(monkeypatch, tmp_path):
"""Keep the CLI callback from writing run logs into the project `output/logs`.
The callback builds `LOGGING_PATH / <timestamp>.log` on every command call,
so `runner.invoke()` would otherwise create a file in the working tree and
add two handlers to the root logger for each test.
"""
monkeypatch.setattr("logwatcher.cli.LOGGING_PATH", tmp_path / "logs")
@pytest.fixture(name="original_log_dir")
def original_log_dir_fixture() -> Path:
"""Return log directory containing real logs.
+78 -5
View File
@@ -9,6 +9,8 @@ from unittest.mock import MagicMock
from logwatcher import __version__
from logwatcher.cli import _get_files_from_dir, app
from logwatcher.config import RESULT_PATH
from logwatcher.utils import is_log_file
runner = CliRunner()
@@ -35,9 +37,7 @@ def get_argument_from_files(log_dir: Path, output_path: Path) -> list[str]:
List of arguments in string format
"""
files = [
str(file) for file in log_dir.iterdir() if file.suffix in ("", ".txt", ".log")
]
files = [str(file) for file in log_dir.iterdir() if is_log_file(file)]
arguments = ["from-files", "--output-dir", str(output_path)]
for file in files:
@@ -384,24 +384,28 @@ def test_cli_from_files_valid_logs(output_path: Path, valid_log_dir: Path):
def test_cli_from_mails_no_output_dir(monkeypatch, make_mock_account):
"""Assert everything works when no output_path is provided.
Must use "output/" directory, located in project root.
Must use `RESULT_PATH` as the default output directory.
"""
messages = [
Message(subject="subject test 1", text_body="body test 1"),
Message(subject="subject test 2", text_body="body test 2"),
Message(subject="subject test 3", text_body="body test 3"),
]
write_log_report_mock = MagicMock()
with monkeypatch.context() as m:
m.setattr(
"logwatcher.cli.connect_to_mailbox",
lambda: make_mock_account(messages=messages, analyzed_folder="Analyzed"),
)
m.setattr("logwatcher.notifier.Message.send", MagicMock(return_value=None))
m.setattr("logwatcher.cli.write_log_report", write_log_report_mock)
m.setattr("logwatcher.cli.send_n2_report", MagicMock())
arguments = ["from-mails"]
results = runner.invoke(app, arguments)
assert results.exit_code == 0
assert write_log_report_mock.call_args.kwargs["output_dir"] == RESULT_PATH
def test_cli_from_mails_invalid_output_dir(tmp_path, monkeypatch):
@@ -465,3 +469,72 @@ def test_cli_from_mails_valid_output_dir(tmp_path, monkeypatch, make_mock_accoun
assert len(output_children) == len(expected)
for name in expected:
assert name in file_names
def test_prune_help_lists_targets():
"""Assert the prune command lists both of its targets."""
results = runner.invoke(app, ["prune", "--help"])
assert results.exit_code == 0
assert "--mails" in results.output
assert "--logs" in results.output
def test_prune_without_target_raises():
"""Assert that prune without any target is rejected."""
results = runner.invoke(app, ["prune"])
assert results.exit_code == 2
assert "At least one of --mails or --logs must be provided." in results.output
def test_prune_logs_only(monkeypatch):
"""Assert `--logs` prunes logs only, without connecting to the mailbox."""
connect_mock = MagicMock()
prune_mails_mock = MagicMock()
get_logs_mock = MagicMock(return_value=[MagicMock()])
archive_logs_mock = MagicMock()
prune_logs_mock = MagicMock()
with monkeypatch.context() as m:
m.setattr("logwatcher.cli.connect_to_mailbox", connect_mock)
m.setattr("logwatcher.cli.prune_mails", prune_mails_mock)
m.setattr("logwatcher.cli.get_logs_to_archive", get_logs_mock)
m.setattr("logwatcher.cli.archive_logs", archive_logs_mock)
m.setattr("logwatcher.cli.prune_logs", prune_logs_mock)
results = runner.invoke(app, ["prune", "--logs"])
assert results.exit_code == 0
get_logs_mock.assert_called_once()
archive_logs_mock.assert_called_once()
prune_logs_mock.assert_called_once()
connect_mock.assert_not_called()
prune_mails_mock.assert_not_called()
def test_prune_mails_only(monkeypatch):
"""Assert `--mails` prunes mails only, without touching log archives."""
fake_account = MagicMock()
connect_mock = MagicMock(return_value=fake_account)
prune_mails_mock = MagicMock()
get_logs_mock = MagicMock()
archive_logs_mock = MagicMock()
prune_logs_mock = MagicMock()
with monkeypatch.context() as m:
m.setattr("logwatcher.cli.connect_to_mailbox", connect_mock)
m.setattr("logwatcher.cli.prune_mails", prune_mails_mock)
m.setattr("logwatcher.cli.get_logs_to_archive", get_logs_mock)
m.setattr("logwatcher.cli.archive_logs", archive_logs_mock)
m.setattr("logwatcher.cli.prune_logs", prune_logs_mock)
results = runner.invoke(app, ["prune", "--mails"])
assert results.exit_code == 0
connect_mock.assert_called_once()
prune_mails_mock.assert_called_once()
assert prune_mails_mock.call_args.args[1] is fake_account
get_logs_mock.assert_not_called()
archive_logs_mock.assert_not_called()
prune_logs_mock.assert_not_called()
-15
View File
@@ -114,9 +114,6 @@ class OkAccount:
self.protocol = OkProtocol()
#########################################
# _find_log_attachment #
#########################################
def test_find_log_attachment_returns_log_file(
empty_mail: Message,
):
@@ -221,9 +218,6 @@ def test_find_log_attachment_rejects_non_file_attachment(empty_mail: Message):
assert attachment is None
############################
# _get_attachment_content #
############################
def test_get_attachment_content_decodes_windows_1252(original_log_dir: Path):
"""Decodes content with accents correctly."""
file_with_accents = original_log_dir / "CR_20260727110008.txt"
@@ -249,9 +243,6 @@ def test_get_attachment_content_empty_bytes():
assert content == ""
########################
# _get_mail_body(mail) #
########################
def test_get_mail_body_from_marker_to_end(empty_mail: Message):
"""Returns body starting at 'Répertoire scanné'."""
empty_mail.text_body += "Répertoire scanné : \\192.168.60.40\\e$\\MDC_1110\\Logs"
@@ -291,9 +282,6 @@ def test_get_mail_body_multiple_markers(empty_mail: Message):
assert "Répertoire scanné\nRépertoire scanné\nRépertoire scanné" == content
################################
# _has_attachment_marker(body) #
################################
def test_has_attachment_marker_true():
"""Returns True when the exact pattern is present."""
body_with_pattern = "Compte-rendu du suivi des imports NOSYMAG du 02/09/2026 à 15:00:10\n\nLe compte-rendu contient plus de 100 lignes.\nSe reporter à la pièce jointe."
@@ -318,9 +306,6 @@ def test_has_attachment_marker_empty_string():
assert not _has_attachment_marker(body_empty)
########################
# connect_to_mailbox() #
########################
def test_connect_to_mailbox_success(monkeypatch: MonkeyPatch, caplog, set_env):
"""Returns an Account when credentials are valid."""
with monkeypatch.context() as m, caplog.at_level(logging.INFO, "logwatcher"):
+1 -1
View File
@@ -235,7 +235,7 @@ def _test_write_log_report(
SourceType.FILE,
relevant,
irrelevant,
datetime.now(FRENCH_TIMEZONE).strftime(DATETIME_FORMAT),
today,
start_date,
end_date,
nb_files,