test(cli): ✅ add tests for prune command
add tests for prune command and some code optimization and formatting.
This commit is contained in:
@@ -34,6 +34,17 @@ def tmp_setup_logging(tmp_log_file):
|
|||||||
setup_logging(file_path=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")
|
@pytest.fixture(name="original_log_dir")
|
||||||
def original_log_dir_fixture() -> Path:
|
def original_log_dir_fixture() -> Path:
|
||||||
"""Return log directory containing real logs.
|
"""Return log directory containing real logs.
|
||||||
|
|||||||
+79
-6
@@ -9,6 +9,8 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
from logwatcher import __version__
|
from logwatcher import __version__
|
||||||
from logwatcher.cli import _get_files_from_dir, app
|
from logwatcher.cli import _get_files_from_dir, app
|
||||||
|
from logwatcher.config import RESULT_PATH
|
||||||
|
from logwatcher.utils import is_log_file
|
||||||
|
|
||||||
runner = CliRunner()
|
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
|
List of arguments in string format
|
||||||
|
|
||||||
"""
|
"""
|
||||||
files = [
|
files = [str(file) for file in log_dir.iterdir() if is_log_file(file)]
|
||||||
str(file) for file in log_dir.iterdir() if file.suffix in ("", ".txt", ".log")
|
|
||||||
]
|
|
||||||
|
|
||||||
arguments = ["from-files", "--output-dir", str(output_path)]
|
arguments = ["from-files", "--output-dir", str(output_path)]
|
||||||
for file in files:
|
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):
|
def test_cli_from_mails_no_output_dir(monkeypatch, make_mock_account):
|
||||||
"""Assert everything works when no output_path is provided.
|
"""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 = [
|
messages = [
|
||||||
Message(subject="subject test 1", text_body="body test 1"),
|
Message(subject="subject test 1", text_body="body test 1"),
|
||||||
Message(subject="subject test 2", text_body="body test 2"),
|
Message(subject="subject test 2", text_body="body test 2"),
|
||||||
Message(subject="subject test 3", text_body="body test 3"),
|
Message(subject="subject test 3", text_body="body test 3"),
|
||||||
]
|
]
|
||||||
|
write_log_report_mock = MagicMock()
|
||||||
|
|
||||||
with monkeypatch.context() as m:
|
with monkeypatch.context() as m:
|
||||||
m.setattr(
|
m.setattr(
|
||||||
"logwatcher.cli.connect_to_mailbox",
|
"logwatcher.cli.connect_to_mailbox",
|
||||||
lambda: make_mock_account(messages=messages, analyzed_folder="Analyzed"),
|
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"]
|
arguments = ["from-mails"]
|
||||||
results = runner.invoke(app, arguments)
|
results = runner.invoke(app, arguments)
|
||||||
assert results.exit_code == 0
|
|
||||||
|
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):
|
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)
|
assert len(output_children) == len(expected)
|
||||||
for name in expected:
|
for name in expected:
|
||||||
assert name in file_names
|
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()
|
||||||
|
|||||||
@@ -114,9 +114,6 @@ class OkAccount:
|
|||||||
self.protocol = OkProtocol()
|
self.protocol = OkProtocol()
|
||||||
|
|
||||||
|
|
||||||
#########################################
|
|
||||||
# _find_log_attachment #
|
|
||||||
#########################################
|
|
||||||
def test_find_log_attachment_returns_log_file(
|
def test_find_log_attachment_returns_log_file(
|
||||||
empty_mail: Message,
|
empty_mail: Message,
|
||||||
):
|
):
|
||||||
@@ -221,9 +218,6 @@ def test_find_log_attachment_rejects_non_file_attachment(empty_mail: Message):
|
|||||||
assert attachment is None
|
assert attachment is None
|
||||||
|
|
||||||
|
|
||||||
############################
|
|
||||||
# _get_attachment_content #
|
|
||||||
############################
|
|
||||||
def test_get_attachment_content_decodes_windows_1252(original_log_dir: Path):
|
def test_get_attachment_content_decodes_windows_1252(original_log_dir: Path):
|
||||||
"""Decodes content with accents correctly."""
|
"""Decodes content with accents correctly."""
|
||||||
file_with_accents = original_log_dir / "CR_20260727110008.txt"
|
file_with_accents = original_log_dir / "CR_20260727110008.txt"
|
||||||
@@ -249,9 +243,6 @@ def test_get_attachment_content_empty_bytes():
|
|||||||
assert content == ""
|
assert content == ""
|
||||||
|
|
||||||
|
|
||||||
########################
|
|
||||||
# _get_mail_body(mail) #
|
|
||||||
########################
|
|
||||||
def test_get_mail_body_from_marker_to_end(empty_mail: Message):
|
def test_get_mail_body_from_marker_to_end(empty_mail: Message):
|
||||||
"""Returns body starting at 'Répertoire scanné'."""
|
"""Returns body starting at 'Répertoire scanné'."""
|
||||||
empty_mail.text_body += "Répertoire scanné : \\192.168.60.40\\e$\\MDC_1110\\Logs"
|
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
|
assert "Répertoire scanné\nRépertoire scanné\nRépertoire scanné" == content
|
||||||
|
|
||||||
|
|
||||||
################################
|
|
||||||
# _has_attachment_marker(body) #
|
|
||||||
################################
|
|
||||||
def test_has_attachment_marker_true():
|
def test_has_attachment_marker_true():
|
||||||
"""Returns True when the exact pattern is present."""
|
"""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."
|
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)
|
assert not _has_attachment_marker(body_empty)
|
||||||
|
|
||||||
|
|
||||||
########################
|
|
||||||
# connect_to_mailbox() #
|
|
||||||
########################
|
|
||||||
def test_connect_to_mailbox_success(monkeypatch: MonkeyPatch, caplog, set_env):
|
def test_connect_to_mailbox_success(monkeypatch: MonkeyPatch, caplog, set_env):
|
||||||
"""Returns an Account when credentials are valid."""
|
"""Returns an Account when credentials are valid."""
|
||||||
with monkeypatch.context() as m, caplog.at_level(logging.INFO, "logwatcher"):
|
with monkeypatch.context() as m, caplog.at_level(logging.INFO, "logwatcher"):
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ def _test_write_log_report(
|
|||||||
SourceType.FILE,
|
SourceType.FILE,
|
||||||
relevant,
|
relevant,
|
||||||
irrelevant,
|
irrelevant,
|
||||||
datetime.now(FRENCH_TIMEZONE).strftime(DATETIME_FORMAT),
|
today,
|
||||||
start_date,
|
start_date,
|
||||||
end_date,
|
end_date,
|
||||||
nb_files,
|
nb_files,
|
||||||
|
|||||||
Reference in New Issue
Block a user