test(notifier): ✅ add tests for notifier module
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
@@ -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 []
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user