diff --git a/django_courriel/md.py b/django_courriel/md.py index b1f6d5b..613cc21 100644 --- a/django_courriel/md.py +++ b/django_courriel/md.py @@ -29,6 +29,8 @@ from .validation import ( ValidAddressList, ) +InvalidAddress = InvalidAddressError + logger = logging.getLogger(__name__) @@ -72,6 +74,44 @@ class MarkdownTemplate: self.raw_content = self.load_raw_template(template_name) self.metadata, self.md_body = self.parse_template(self.raw_content) self.validate() + self._from_email = self._init_from_email() + self._to = self._init_address_list("to") + self._cc = self._init_address_list("cc") + self._bcc = self._init_address_list("bcc") + self._reply_to = self._init_address_list("reply_to") + + def _init_from_email(self) -> ValidAddress | None: + raw_from = self.metadata.get("from_email") + if raw_from: + return ValidAddress.factory(raw_from) + return None + + def _init_address_list(self, key: str) -> ValidAddressList: + raw_val = self.metadata.get(key) + if not raw_val: + return ValidAddressList() + items = [raw_val] if isinstance(raw_val, str) else list(raw_val) + return ValidAddressList(items) + + @property + def from_email(self) -> ValidAddress | None: + return self._from_email + + @property + def to(self) -> ValidAddressList: + return self._to + + @property + def cc(self) -> ValidAddressList: + return self._cc + + @property + def bcc(self) -> ValidAddressList: + return self._bcc + + @property + def reply_to(self) -> ValidAddressList: + return self._reply_to @classmethod def load_raw_template(cls, template_name: str) -> str: diff --git a/django_courriel/templates/courriel/base.dj.html b/django_courriel/templates/courriel/base.dj.html index 461ef11..8dae876 100644 --- a/django_courriel/templates/courriel/base.dj.html +++ b/django_courriel/templates/courriel/base.dj.html @@ -6,6 +6,6 @@ -{% block body %}{{ content_html|safe }}{% endblock %} +{% block body %}{{ content_html|safe }}{% endblock %} diff --git a/tests/templates/invalid_from_email.md b/tests/templates/invalid_from_email.md new file mode 100644 index 0000000..3cacfd2 --- /dev/null +++ b/tests/templates/invalid_from_email.md @@ -0,0 +1,5 @@ +--- +subject: "Test from_email invalide" +from_email: "invalid-email-address" +--- +Contenu de test. diff --git a/tests/templates/invalid_recipient.md b/tests/templates/invalid_recipient.md new file mode 100644 index 0000000..c61dced --- /dev/null +++ b/tests/templates/invalid_recipient.md @@ -0,0 +1,7 @@ +--- +subject: "Test adresse invalide dans to" +to: + - "valid@example.org" + - "bad address" +--- +Contenu de test. diff --git a/tests/templates/missing_subject.md b/tests/templates/missing_subject.md new file mode 100644 index 0000000..7c81144 --- /dev/null +++ b/tests/templates/missing_subject.md @@ -0,0 +1,4 @@ +--- +to: "test@example.com" +--- +Corps de test sans sujet. diff --git a/tests/templates/test_missing_subject_var.md b/tests/templates/test_missing_subject_var.md deleted file mode 100644 index 87ba6da..0000000 --- a/tests/templates/test_missing_subject_var.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -subject: "Notification pour {{ missing_user }}" -to: "test@example.com" ---- -Corps de test. diff --git a/tests/templates/valid_addresses.md b/tests/templates/valid_addresses.md new file mode 100644 index 0000000..52fc2f0 --- /dev/null +++ b/tests/templates/valid_addresses.md @@ -0,0 +1,11 @@ +--- +subject: "Test adresses" +to: + - "Victor Hugo " + - "antoine@saint-exupery.aero" +cc: "Aimé Césaire " +bcc: + - "boris@vian.jazz" +reply_to: "contact@example.org" +--- +Contenu de test pour les adresses. diff --git a/tests/templates/valid_from_email.md b/tests/templates/valid_from_email.md new file mode 100644 index 0000000..25cc9c4 --- /dev/null +++ b/tests/templates/valid_from_email.md @@ -0,0 +1,5 @@ +--- +subject: "Test from_email valide" +from_email: "Victor Hugo " +--- +Contenu de test. diff --git a/tests/test_md.py b/tests/test_md.py index 59f1712..af21c2f 100644 --- a/tests/test_md.py +++ b/tests/test_md.py @@ -1,16 +1,106 @@ +import unittest +from django.template.exceptions import TemplateDoesNotExist from django.test import SimpleTestCase +from django_courriel.md import MarkdownTemplate +from django_courriel.validation import ( + InvalidAddressError, + InvalidFrontMatter, + ValidAddress, + ValidAddressList, +) + class MarkdownTemplateTest(SimpleTestCase): - def test_init(self): - raise NotImplementedError() + def test_init_valid(self): + """A valid markdown template initializes with metadata and markdown body.""" + template = MarkdownTemplate("minimal.md") + self.assertEqual(template.template_name, "minimal.md") + self.assertIn("subject", template.metadata) + self.assertEqual(template.metadata["subject"], "Hello") + self.assertIn("Hello world !", template.md_body) - def test_minimal(self): - raise NotImplementedError() + def test_init_template_does_not_exist(self): + """Instantiating with an unresolvable template raises TemplateDoesNotExist.""" + with self.assertRaises(TemplateDoesNotExist): + MarkdownTemplate("does_not_exist.md") + def test_init_missing_front_matter(self): + """A template without frontmatter delimiters raises InvalidFrontMatter.""" + with self.assertRaises(InvalidFrontMatter): + MarkdownTemplate("missing_front_matter.md") + + def test_init_invalid_front_matter(self): + """A template with malformed frontmatter raises InvalidFrontMatter.""" + with self.assertRaises(InvalidFrontMatter): + MarkdownTemplate("invalid_front_matter.md") + + def test_init_missing_subject(self): + """A template frontmatter without a 'subject' key raises InvalidFrontMatter.""" + with self.assertRaises(InvalidFrontMatter) as ctx: + MarkdownTemplate("missing_subject.md") + self.assertIn("subject", str(ctx.exception).lower()) + + def test_from_email_none_when_omitted(self): + """When from_email is not specified in frontmatter, getter returns None.""" + template = MarkdownTemplate("minimal.md") + self.assertIsNone(template.from_email) + + def test_from_email_valid(self): + """When from_email is valid in frontmatter, getter returns a ValidAddress instance.""" + template = MarkdownTemplate("valid_from_email.md") + self.assertIsInstance(template.from_email, ValidAddress) + self.assertEqual(template.from_email.display_name, "Victor Hugo") + self.assertEqual(template.from_email.addr_spec, "hugo@les-miserables.fr") + + def test_from_email_invalid_raises_error(self): + """When from_email is invalid in frontmatter, __init__ raises InvalidAddressError.""" + with self.assertRaises(InvalidAddressError): + MarkdownTemplate("invalid_from_email.md") + + def test_recipient_addresses_empty_when_omitted_or_empty(self): + """When to, cc, bcc, reply_to are omitted or empty, getters return empty ValidAddressList instances.""" + template = MarkdownTemplate("minimal.md") + + self.assertIsInstance(template.to, ValidAddressList) + self.assertEqual(len(template.to), 0) + + self.assertIsInstance(template.cc, ValidAddressList) + self.assertEqual(len(template.cc), 0) + + self.assertIsInstance(template.bcc, ValidAddressList) + self.assertEqual(len(template.bcc), 0) + + self.assertIsInstance(template.reply_to, ValidAddressList) + self.assertEqual(len(template.reply_to), 0) + + def test_recipient_addresses_valid(self): + """When address lists or single addresses are defined, getters return ValidAddressList.""" + template = MarkdownTemplate("valid_addresses.md") + + self.assertIsInstance(template.to, ValidAddressList) + self.assertEqual(len(template.to), 2) + self.assertEqual(template.to[0].display_name, "Victor Hugo") + self.assertEqual(template.to[1].addr_spec, "antoine@saint-exupery.aero") + + self.assertIsInstance(template.cc, ValidAddressList) + self.assertEqual(len(template.cc), 1) + self.assertEqual(template.cc[0].addr_spec, "cesaire@cahiers.net") + + self.assertIsInstance(template.bcc, ValidAddressList) + self.assertEqual(len(template.bcc), 1) + self.assertEqual(template.bcc[0].addr_spec, "boris@vian.jazz") + + self.assertIsInstance(template.reply_to, ValidAddressList) + self.assertEqual(len(template.reply_to), 1) + self.assertEqual(template.reply_to[0].addr_spec, "contact@example.org") + + def test_recipient_addresses_invalid_raises_error(self): + """When any recipient contains an invalid address, __init__ raises InvalidAddressError.""" + with self.assertRaises(InvalidAddressError): + MarkdownTemplate("invalid_recipient.md") + + @unittest.skip("Will be implemented in the rendering slice") def test_subject_rendering(self): raise NotImplementedError() - - def test_addresses(self): - raise NotImplementedError()