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_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_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",
|
"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_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>\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_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+)",
|
"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",
|
"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",
|
"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:
|
try:
|
||||||
to = os.environ["N2_REPORT_RECIPIENTS"].split(",")
|
to = os.environ["N2_REPORT_RECIPIENTS"].replace(" ", "").split(",")
|
||||||
subject = os.environ["N2_REPORT_SUBJECT"]
|
subject = os.environ["N2_REPORT_SUBJECT"]
|
||||||
except KeyError as error:
|
except KeyError as error:
|
||||||
logger.error(f"{error.args[0]} was not found in environment variable.")
|
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}'...")
|
logger.info(f"Attempt to send mail '{mail.subject}'...")
|
||||||
mail.send(copy_to_folder=sent_folder)
|
mail.send(copy_to_folder=sent_folder)
|
||||||
logger.info(f"Mail '{subject}' was successfully sent to '{to}'.")
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def get_or_create_folder(account: Account, folder_name: str) -> Folder:
|
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:
|
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:
|
Returns:
|
||||||
The existing or newly created 'Analyzed' folder.
|
The existing or newly created 'Analyzed' folder.
|
||||||
|
|||||||
+23
-15
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from exchangelib import Message
|
from exchangelib import Account, Message
|
||||||
|
|
||||||
from logwatcher.config import FIXTURE_PATH
|
from logwatcher.config import FIXTURE_PATH
|
||||||
from logwatcher.logging_config import setup_logging
|
from logwatcher.logging_config import setup_logging
|
||||||
@@ -23,7 +23,8 @@ os.environ["PYTEST_DEBUG_TEMPROOT"] = str(Path("output/test").absolute())
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tmp_log_file(tmp_path):
|
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)
|
@pytest.fixture(autouse=True)
|
||||||
def tmp_setup_logging(tmp_log_file):
|
def tmp_setup_logging(tmp_log_file):
|
||||||
@@ -32,39 +33,46 @@ def tmp_setup_logging(tmp_log_file):
|
|||||||
|
|
||||||
@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.
|
||||||
All real log files provided by N2 technicians.
|
|
||||||
Used for integration tests: these files contain both valid lines
|
All real log files provided by N2 technicians.
|
||||||
and lines to be ignored (system logs, empty lines, directory headers).
|
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"
|
return FIXTURE_PATH / "original_logs"
|
||||||
|
|
||||||
@pytest.fixture(name="valid_log_dir")
|
@pytest.fixture(name="valid_log_dir")
|
||||||
def valid_log_dir_fixture() -> Path:
|
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
|
All valid log files provided by N2 technicians.
|
||||||
for N2 technicians.
|
Used for integration tests: these files contain both relevant and irrelevant logs
|
||||||
|
for N2 technicians.
|
||||||
"""
|
"""
|
||||||
return FIXTURE_PATH / "valid_logs"
|
return FIXTURE_PATH / "valid_logs"
|
||||||
|
|
||||||
@pytest.fixture(name="invalid_log_dir")
|
@pytest.fixture(name="invalid_log_dir")
|
||||||
def invalid_log_dir_fixture() -> Path:
|
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.
|
All invalid log files created for testing only.
|
||||||
|
Used for integration tests of parsing.
|
||||||
"""
|
"""
|
||||||
return FIXTURE_PATH / "invalid_logs"
|
return FIXTURE_PATH / "invalid_logs"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="package")
|
@pytest.fixture(scope="package")
|
||||||
def make_mock_account():
|
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(
|
def _make_mock_account(
|
||||||
messages: list[Message] | None = None, analyzed_folder: MagicMock | None = None
|
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 = account.msg_folder_root.__truediv__.return_value
|
||||||
logs_folder.all.return_value = messages or []
|
logs_folder.all.return_value = messages or []
|
||||||
logs_folder.glob.return_value = [analyzed_folder] if analyzed_folder else []
|
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")
|
@pytest.fixture(name="empty_mail")
|
||||||
def get_empty_mail_fixture() -> Message:
|
def get_empty_mail_fixture() -> Message:
|
||||||
"""A fake Message"""
|
"""Return a fake Message."""
|
||||||
return Message(subject="test without attachment", text_body="test")
|
return Message(subject="test without attachment", text_body="test")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="log_mail_with_attachment")
|
@pytest.fixture(name="log_mail_with_attachment")
|
||||||
def get_log_mail_with_attachment(original_log_dir: Path) -> Message:
|
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"
|
file = original_log_dir / "CR_20260727110008.txt"
|
||||||
|
|
||||||
return Message(
|
return Message(
|
||||||
@@ -49,7 +49,7 @@ def get_log_mail_with_attachment(original_log_dir: Path) -> Message:
|
|||||||
|
|
||||||
@pytest.fixture(name="log_mail_with_body")
|
@pytest.fixture(name="log_mail_with_body")
|
||||||
def get_log_mail_with_body() -> Message:
|
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(
|
return Message(
|
||||||
subject="test with logs in text body",
|
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"
|
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."""
|
"""Protocol whose .version access raises UnauthorizedError."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self): # noqa: D102
|
||||||
raise UnauthorizedError("invalid credentials")
|
raise UnauthorizedError("invalid credentials")
|
||||||
|
|
||||||
|
|
||||||
class FakeAccount:
|
class FakeAccount:
|
||||||
"""Account with a protocol that raises on version access."""
|
"""Account with a protocol that raises on version access."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self): # noqa: D107
|
||||||
self.protocol = FakeProtocol()
|
self.protocol = FakeProtocol()
|
||||||
|
|
||||||
|
|
||||||
@@ -103,14 +103,14 @@ class OkProtocol:
|
|||||||
"""Protocol whose .version succeeds."""
|
"""Protocol whose .version succeeds."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self): # noqa: D102
|
||||||
return MagicMock()
|
return MagicMock()
|
||||||
|
|
||||||
|
|
||||||
class OkAccount:
|
class OkAccount:
|
||||||
"""Account with a protocol that returns a version successfully."""
|
"""Account with a protocol that returns a version successfully."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self): # noqa: D102
|
||||||
self.protocol = OkProtocol()
|
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(
|
def test_find_log_attachment_rejects_other_names(
|
||||||
empty_mail: Message,
|
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 = (
|
attachments = (
|
||||||
FileAttachment(
|
FileAttachment(
|
||||||
name="attachment.pdf",
|
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