From cf09ab0c6ac1518500c53aad7e4e04a9cb6ada5a Mon Sep 17 00:00:00 2001 From: maurane Date: Wed, 16 Sep 2026 14:46:35 +0200 Subject: [PATCH] =?UTF-8?q?test(notifier):=20=E2=9C=85=20add=20tests=20for?= =?UTF-8?q?=20notifier=20module?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/logwatcher/classifier.py | 4 ++-- src/logwatcher/notifier.py | 4 ++-- src/logwatcher/utils.py | 6 ++--- tests/conftest.py | 38 +++++++++++++++++------------ tests/test_mail_reader.py | 19 +++++++-------- tests/test_notifier.py | 46 ++++++++++++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 33 deletions(-) create mode 100644 tests/test_notifier.py diff --git a/src/logwatcher/classifier.py b/src/logwatcher/classifier.py index 1356ea0..d8d991d 100644 --- a/src/logwatcher/classifier.py +++ b/src/logwatcher/classifier.py @@ -12,8 +12,8 @@ N2_PATTERNS = { "CLOTHES_SIZE_RANGE_ORDER": r"Erreur : (?PGRP_\d+) / (?P\d{5}/\d{3}) (?P\d{4})_(?P\w+) ERREUR (?P(?P\w+ (?P\w+))(?P\w{3})?) / SetTailleTrav -> SizeRange (?P\d+\.\d+) / Dépassement du nombre de tailles travaillées \(28 tailles maximum\)", "CLOTHES_SIZE_RANGE_BL": r"Erreur : (?P(?P\d{4})_(?P[\w-]+)_DESADV) : / (?P\d{5}/\d{3}) .*? SizeRange (?P\d+\.\d+) .*?Dépassement du nombre de tailles travaillées \(28 tailles maximum\)", "CLOTHES_SIZE_NOT_FOUND": r"(?PGRP_\d+) / (?P\d{5}/\d{3}) (?P\d{4})_(?P\w+) WARNING \d+ \w+ / SizeRange (?P\d+\.\d+) - Columnx \d+ / Taille In[ée]xistante", - "BL_AUTO_BESTSELLER_REJECTED": r"Erreur : (?P\d{4}_\d{10}_DESADV) : / (?P\d{5}/\d{3}) (\d{4}_\d{10}_DESADV) ERREUR [\w\_]+ / GETINFOLIGNE_FOU -> BL AUTO : \w+ / Package : / EAN : (?P\w+) / Aucune ligne de la commande (?P\w+) ne correspond", - "BL_COMMAND_NOT_FOUND": r"(?P\d{4}_\d{10}_DESADV) : / (?P\d{5}/\d{3}) (\d{4}_\d{10}_DESADV) ERREUR [\w\_]+ / GETINFOLIGNE_FOU -> BL AUTO : \w+ / Package : / EAN : (?P\w+) / La commande (?P\w+) n'a pas été trouvé", + "BL_AUTO_BESTSELLER_REJECTED": r"Erreur : (?P\d{4}_\d{8,}_DESADV) : / (?P\d{5}/\d{3}) (\d{4}_\d{10}_DESADV) ERREUR [\w\_]+ / GETINFOLIGNE_FOU -> BL AUTO : \w+ / Package : / EAN : (?P\w+) / Aucune ligne de la commande (?P\w+) ne correspond", + "BL_COMMAND_NOT_FOUND": r"(?P(?P\d{4})_\d{8,}_DESADV) .+ / La commande (?P\w+) n'a pas été trouvé", "FEDAS_CODE_NOT_FOUND": r"Exception : Code Fedas non trouvé : (?P\d+)", "IMPOSSIBLE_FUSION_DIFFERENT_MODEL_SIZE": r"Erreur : (?PGRP_\d+) / (?P\d{5}/\d{3}) (?P\d{4})_(?P\w+) WARNING \w+ \w+ / Art_id : \w+ - Fusart_id : (?P\w+) / Impossible de créer un lien", "MODEL_CODE_DOES_NOT_MATCH_EAN_CODE": r"Erreur : (?P(?P\d{4})_(?P[\w-]+)_DESADV) : / (?P\d{5}/\d{3}) \d{4}_\w+ ERREUR (?P\w+ \w+) / Article -> BL AUTO : (?P\w+) / Package : (?P\w+) / EAN : (?P\d+) / Article création erreur", diff --git a/src/logwatcher/notifier.py b/src/logwatcher/notifier.py index 8f49b80..ebd15d7 100644 --- a/src/logwatcher/notifier.py +++ b/src/logwatcher/notifier.py @@ -19,7 +19,7 @@ def send_n2_report(account: Account, summary: str, n2_log_file: Path) -> None: """ try: - to = os.environ["N2_REPORT_RECIPIENTS"].split(",") + to = os.environ["N2_REPORT_RECIPIENTS"].replace(" ", "").split(",") subject = os.environ["N2_REPORT_SUBJECT"] except KeyError as error: logger.error(f"{error.args[0]} was not found in environment variable.") @@ -43,4 +43,4 @@ def send_n2_report(account: Account, summary: str, n2_log_file: Path) -> None: logger.info(f"Attempt to send mail '{mail.subject}'...") mail.send(copy_to_folder=sent_folder) logger.info(f"Mail '{subject}' was successfully sent to '{to}'.") - logger.info(f"Mail is saved in '{sent_folder}' folder.") \ No newline at end of file + logger.info(f"Mail is saved in '{sent_folder}' folder.") diff --git a/src/logwatcher/utils.py b/src/logwatcher/utils.py index 9a84ae1..4a219a6 100644 --- a/src/logwatcher/utils.py +++ b/src/logwatcher/utils.py @@ -5,11 +5,11 @@ from exchangelib import Account, Folder logger = logging.getLogger(__name__) def get_or_create_folder(account: Account, folder_name: str) -> Folder: - """ - Return the 'Analyzed' folder, creating it if it doesn't exist. + """Return the 'Analyzed' folder, creating it if it doesn't exist. Args: - account: The mailbox account owning the Logs folder. + account: The mailbox account owning the Logs folder + folder_name: Name of the account folder to find Returns: The existing or newly created 'Analyzed' folder. diff --git a/tests/conftest.py b/tests/conftest.py index c35acc6..0204cdd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ from pathlib import Path from unittest.mock import MagicMock import pytest -from exchangelib import Message +from exchangelib import Account, Message from logwatcher.config import FIXTURE_PATH from logwatcher.logging_config import setup_logging @@ -23,7 +23,8 @@ os.environ["PYTEST_DEBUG_TEMPROOT"] = str(Path("output/test").absolute()) @pytest.fixture def tmp_log_file(tmp_path): - return tmp_path / "logwatcher_test.log" + """Return path of logs logwatcher related.""" + return tmp_path / "logwatcher_test.log" @pytest.fixture(autouse=True) def tmp_setup_logging(tmp_log_file): @@ -32,39 +33,46 @@ def tmp_setup_logging(tmp_log_file): @pytest.fixture(name="original_log_dir") def original_log_dir_fixture() -> Path: - """ - All real log files provided by N2 technicians. - Used for integration tests: these files contain both valid lines - and lines to be ignored (system logs, empty lines, directory headers). + """Return log directory containing real logs. + + All real log files provided by N2 technicians. + Used for integration tests: these files contain both valid lines + and lines to be ignored (system logs, empty lines, directory headers). """ return FIXTURE_PATH / "original_logs" @pytest.fixture(name="valid_log_dir") def valid_log_dir_fixture() -> Path: - """ - All valid log files provided by N2 technicians. - Used for integration tests: these files contain both relevant and irrelevant logs - for N2 technicians. + """Return log directory containing only valid logs. + + All valid log files provided by N2 technicians. + Used for integration tests: these files contain both relevant and irrelevant logs + for N2 technicians. """ return FIXTURE_PATH / "valid_logs" @pytest.fixture(name="invalid_log_dir") def invalid_log_dir_fixture() -> Path: - """ - All invalid log files created for testing only. - Used for integration tests of parsing. + """Return log directory containing invalid logs. + + All invalid log files created for testing only. + Used for integration tests of parsing. """ return FIXTURE_PATH / "invalid_logs" @pytest.fixture(scope="package") def make_mock_account(): - """A fake Account with a msg_folder_root returning a fake Logs folder.""" + """Create a fake Account. + + A fake Account with a msg_folder_root returning a fake Logs folder. + """ def _make_mock_account( messages: list[Message] | None = None, analyzed_folder: MagicMock | None = None ): - account = MagicMock() + account = MagicMock(spec=Account) + account.fullname = "fake" logs_folder = account.msg_folder_root.__truediv__.return_value logs_folder.all.return_value = messages or [] logs_folder.glob.return_value = [analyzed_folder] if analyzed_folder else [] diff --git a/tests/test_mail_reader.py b/tests/test_mail_reader.py index 0714926..0c2b45f 100644 --- a/tests/test_mail_reader.py +++ b/tests/test_mail_reader.py @@ -24,13 +24,13 @@ from logwatcher.mail_reader import ( @pytest.fixture(name="empty_mail") def get_empty_mail_fixture() -> Message: - """A fake Message""" + """Return a fake Message.""" return Message(subject="test without attachment", text_body="test") @pytest.fixture(name="log_mail_with_attachment") def get_log_mail_with_attachment(original_log_dir: Path) -> Message: - """A fake Message with a .txt file attachment.""" + """Return a fake Message with a .txt file attachment.""" file = original_log_dir / "CR_20260727110008.txt" return Message( @@ -49,7 +49,7 @@ def get_log_mail_with_attachment(original_log_dir: Path) -> Message: @pytest.fixture(name="log_mail_with_body") def get_log_mail_with_body() -> Message: - """A fake Message with logs in text body.""" + """Return a fake Message with logs in text body.""" return Message( subject="test with logs in text body", text_body="Compte-rendu du suivi des imports NOSYMAG du 02/09/2026 à 15:00:10\n\n\n" @@ -88,14 +88,14 @@ class FakeProtocol: """Protocol whose .version access raises UnauthorizedError.""" @property - def version(self): + def version(self): # noqa: D102 raise UnauthorizedError("invalid credentials") class FakeAccount: """Account with a protocol that raises on version access.""" - def __init__(self): + def __init__(self): # noqa: D107 self.protocol = FakeProtocol() @@ -103,14 +103,14 @@ class OkProtocol: """Protocol whose .version succeeds.""" @property - def version(self): + def version(self): # noqa: D102 return MagicMock() class OkAccount: """Account with a protocol that returns a version successfully.""" - def __init__(self): + def __init__(self): # noqa: D102 self.protocol = OkProtocol() @@ -154,10 +154,7 @@ def test_find_log_attachment_accepts_no_extension(empty_mail: Message): def test_find_log_attachment_rejects_other_names( empty_mail: Message, ): - """ - Rejects attachments not starting with CR_ even with .txt - or .log extension or extensionless. - """ + """Rejects attachments not starting with CR_ even with .txt or .log extension or extensionless.""" attachments = ( FileAttachment( name="attachment.pdf", diff --git a/tests/test_notifier.py b/tests/test_notifier.py new file mode 100644 index 0000000..65ff1c8 --- /dev/null +++ b/tests/test_notifier.py @@ -0,0 +1,46 @@ +import os + +from unittest.mock import MagicMock + +import pytest + +from logwatcher.notifier import send_n2_report + + +def mock_get_or_create_folder(account, folder_name): # noqa: D103 + return folder_name + + +@pytest.mark.parametrize("vars", [("N2_REPORT_SUBJECT", "N2_REPORT_RECIPIENTS"), ("N2_REPORT_RECIPIENTS", "N2_REPORT_SUBJECT")]) +def test_send_n2_report_missing_recipients_env(vars, monkeypatch, ): + """Assert that missing values in environmnent file raises KeyError.""" + monkeypatch.delenv("N2_REPORT_RECIPIENTS") + monkeypatch.delenv("N2_REPORT_SUBJECT") + monkeypatch.setenv(vars[0], "test") + + with pytest.raises(KeyError) as err: + send_n2_report(account=MagicMock(), summary="summary", n2_log_file=MagicMock()) + + assert vars[1] in err.value.args + + +@pytest.mark.parametrize("recipients", ["mail@client.fr", "mail@ client.fr ", "mail1@client.fr,mail2@client.fr,mail3@client.fr", "mail1@client.fr , mail2@client.fr , mail3@client.fr"]) +def test_send_n2_report_multiple_recipients(recipients, make_mock_account, monkeypatch): + """Assert gathering multiple account as recipients from env file is valid. + + Tests with value having space characters. + Mails must be seperated by a comma. + + Args: + recipients: e-mail recipients in one string + make_mock_account: return a fake account used for tests + monkeypatch: used to change environment variables and functions accross the script + + """ + monkeypatch.setenv("N2_REPORT_SUBJECT", "test") + monkeypatch.setenv("N2_REPORT_RECIPIENTS", "mail@client.fr") + + with monkeypatch.context() as m: + m.setattr("logwatcher.notifier.get_or_create_folder", mock_get_or_create_folder) + m.setattr("logwatcher.notifier.Message.send", MagicMock(id="1", return_value=None)) + send_n2_report(account=make_mock_account(), summary="summary", n2_log_file=MagicMock())