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",
+1 -1
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.")
+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.
+14 -6
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,6 +23,7 @@ os.environ["PYTEST_DEBUG_TEMPROOT"] = str(Path("output/test").absolute())
@pytest.fixture
def tmp_log_file(tmp_path):
"""Return path of logs logwatcher related."""
return tmp_path / "logwatcher_test.log"
@pytest.fixture(autouse=True)
@@ -32,7 +33,8 @@ def tmp_setup_logging(tmp_log_file):
@pytest.fixture(name="original_log_dir")
def original_log_dir_fixture() -> Path:
"""
"""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).
@@ -41,7 +43,8 @@ def original_log_dir_fixture() -> Path:
@pytest.fixture(name="valid_log_dir")
def valid_log_dir_fixture() -> Path:
"""
"""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.
@@ -50,7 +53,8 @@ def valid_log_dir_fixture() -> Path:
@pytest.fixture(name="invalid_log_dir")
def invalid_log_dir_fixture() -> Path:
"""
"""Return log directory containing invalid logs.
All invalid log files created for testing only.
Used for integration tests of parsing.
"""
@@ -59,12 +63,16 @@ def invalid_log_dir_fixture() -> Path:
@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())