test(notifier): ✅ add tests for notifier module

This commit is contained in:
2026-09-16 14:46:35 +02:00
parent eb4ef2339e
commit cf09ab0c6a
6 changed files with 84 additions and 33 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ N2_PATTERNS = {
"CLOTHES_SIZE_RANGE_ORDER": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) ERREUR (?P<model_code>(?P<nosymag_model_code>\w+ (?P<color_code>\w+))(?P<brand_code>\w{3})?) / SetTailleTrav -> SizeRange (?P<size_range>\d+\.\d+) / Dépassement du nombre de tailles travaillées \(28 tailles maximum\)",
"CLOTHES_SIZE_RANGE_BL": r"Erreur : (?P<bl_number>(?P<sap_code>\d{4})_(?P<order_number>[\w-]+)_DESADV) : / (?P<store_code>\d{5}/\d{3}) .*? SizeRange (?P<size_range>\d+\.\d+) .*?Dépassement du nombre de tailles travaillées \(28 tailles maximum\)",
"CLOTHES_SIZE_NOT_FOUND": r"(?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING \d+ \w+ / SizeRange (?P<size_range>\d+\.\d+) - Columnx \d+ / Taille In[ée]xistante",
"BL_AUTO_BESTSELLER_REJECTED": r"Erreur : (?P<bl_number>\d{4}_\d{10}_DESADV) : / (?P<store_code>\d{5}/\d{3}) (\d{4}_\d{10}_DESADV) ERREUR [\w\_]+ / GETINFOLIGNE_FOU -> BL AUTO : \w+ / Package : / EAN : (?P<ean_number>\w+) / Aucune ligne de la commande (?P<command_number>\w+) ne correspond",
"BL_COMMAND_NOT_FOUND": r"(?P<bl_number>\d{4}_\d{10}_DESADV) : / (?P<store_code>\d{5}/\d{3}) (\d{4}_\d{10}_DESADV) ERREUR [\w\_]+ / GETINFOLIGNE_FOU -> BL AUTO : \w+ / Package : / EAN : (?P<ean_number>\w+) / La commande (?P<command_number>\w+) n'a pas été trouvé",
"BL_AUTO_BESTSELLER_REJECTED": r"Erreur : (?P<bl_number>\d{4}_\d{8,}_DESADV) : / (?P<store_code>\d{5}/\d{3}) (\d{4}_\d{10}_DESADV) ERREUR [\w\_]+ / GETINFOLIGNE_FOU -> BL AUTO : \w+ / Package : / EAN : (?P<ean_number>\w+) / Aucune ligne de la commande (?P<command_number>\w+) ne correspond",
"BL_COMMAND_NOT_FOUND": r"(?P<bl_number>(?P<sap_code>\d{4})_\d{8,}_DESADV) .+ / La commande (?P<command_number>\w+) n'a pas été trouvé",
"FEDAS_CODE_NOT_FOUND": r"Exception : Code Fedas non trouvé : (?P<fedas_code>\d+)",
"IMPOSSIBLE_FUSION_DIFFERENT_MODEL_SIZE": r"Erreur : (?P<group_number>GRP_\d+) / (?P<store_code>\d{5}/\d{3}) (?P<sap_code>\d{4})_(?P<order_number>\w+) WARNING \w+ \w+ / Art_id : \w+ - Fusart_id : (?P<fusart_id>\w+) / Impossible de créer un lien",
"MODEL_CODE_DOES_NOT_MATCH_EAN_CODE": r"Erreur : (?P<bl_number>(?P<sap_code>\d{4})_(?P<order_number>[\w-]+)_DESADV) : / (?P<store_code>\d{5}/\d{3}) \d{4}_\w+ ERREUR (?P<model_code>\w+ \w+) / Article -> BL AUTO : (?P<bl_auto_code>\w+) / Package : (?P<package_number>\w+) / EAN : (?P<ean_number>\d+) / Article création erreur",
+2 -2
View File
@@ -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.")
logger.info(f"Mail is saved in '{sent_folder}' folder.")
+3 -3
View File
@@ -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.
+23 -15
View File
@@ -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 []
+8 -11
View File
@@ -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",
+46
View File
@@ -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())