From fb7409422ec2d93560b79da60441a1268df11aa6 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 24 Dec 2025 14:23:12 +0100 Subject: [PATCH 01/30] Moved existing tests to integration --- tests/integration/__init__.py | 1 + tests/{ => integration}/tests.py | 0 2 files changed, 1 insertion(+) create mode 100644 tests/integration/__init__.py rename tests/{ => integration}/tests.py (100%) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..37e3e16 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration tests for the Mailgun API client.""" diff --git a/tests/tests.py b/tests/integration/tests.py similarity index 100% rename from tests/tests.py rename to tests/integration/tests.py From 3015b329142487a9ac57cc8f12c0b10963b74e78 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 24 Dec 2025 14:29:37 +0100 Subject: [PATCH 02/30] Created unit tests folder --- tests/unit/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/unit/__init__.py diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..00e4ea5 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Unit tests for the Mailgun API client.""" From 770d0738f28edaae52459b3f01ea2ff0df0eb47f Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Sat, 27 Dec 2025 20:19:09 +0100 Subject: [PATCH 03/30] Ported MessagesTests --- tests/unit/test_integration_mirror.py | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/unit/test_integration_mirror.py diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py new file mode 100644 index 0000000..f00005b --- /dev/null +++ b/tests/unit/test_integration_mirror.py @@ -0,0 +1,53 @@ +"""Unit tests that mirror sync integration tests with all external resources mocked. + +No real API keys or network calls; uses unittest.mock to patch requests. +Mirrors test classes and test methods from tests/integration/tests.py (sync only). +""" + +from __future__ import annotations + +import unittest +from unittest.mock import MagicMock, patch + +from mailgun.client import Client + + +def mock_response(status_code: int = 200, json_data: dict | None = None) -> MagicMock: + """Build a mock response with status_code and json().""" + resp = MagicMock() + resp.status_code = status_code + resp.json.return_value = json_data if json_data is not None else {} + return resp + + +# Test data constants (no env vars) +DOMAIN = "example.com" +TEST_DOMAIN = "mailgun.wrapper.test123" +AUTH = ("api", "fake-api-key") + + +class MessagesTests(unittest.TestCase): + """Mirror of integration MessagesTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.data = { + "from": "sender@example.com", + "to": "recipient@example.com", + "subject": "Hello", + "text": "Body", + "o:tag": "Python test", + } + + @patch("mailgun.client.requests.post") + def test_post_right_message(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"id": "", "message": "Queued"}) + req = self.client.messages.create(data=self.data, domain=self.domain) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.post") + def test_post_wrong_message(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(400, {"message": "Invalid from"}) + req = self.client.messages.create(data={"from": "sdsdsd"}, domain=self.domain) + self.assertEqual(req.status_code, 400) From 862bf97b28f32d136052eff7334eca04d6be3a60 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Mon, 5 Jan 2026 12:56:44 +0100 Subject: [PATCH 04/30] Ported DomainTests --- tests/unit/test_integration_mirror.py | 283 ++++++++++++++++++++++++++ 1 file changed, 283 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index f00005b..6f1b783 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -51,3 +51,286 @@ def test_post_wrong_message(self, m_post: MagicMock) -> None: m_post.return_value = mock_response(400, {"message": "Invalid from"}) req = self.client.messages.create(data={"from": "sdsdsd"}, domain=self.domain) self.assertEqual(req.status_code, 400) + + +class DomainTests(unittest.TestCase): + """Mirror of integration DomainTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.test_domain = TEST_DOMAIN + self.post_domain_data = {"name": self.test_domain} + self.put_domain_data = {"spam_action": "disabled"} + self.post_domain_creds = { + "login": f"alice_bob@{self.domain}", + "password": "test_new_creds123", + } + self.put_domain_creds = {"password": "test_new_creds"} + self.put_domain_connections_data = {"require_tls": "false", "skip_verification": "false"} + self.put_domain_tracking_data = {"active": "yes", "skip_verification": "false"} + self.put_domain_unsubscribe_data = { + "active": "yes", + "html_footer": "\n
\n

UnSuBsCrIbE

\n", + "text_footer": "\n\nTo unsubscribe here click: <%unsubscribe_url%>\n\n", + } + self.put_domain_dkim_authority_data = {"self": "false"} + self.put_domain_webprefix_data = {"web_prefix": "python"} + self.put_dkim_selector_data = {"dkim_selector": "s"} + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_post_domain(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "ok"}) + m_post.return_value = mock_response( + 200, {"message": "Domain DNS records have been created"} + ) + request = self.client.domains.create(data=self.post_domain_data) + self.assertEqual(request.status_code, 200) + self.assertIn("Domain DNS records have been created", request.json()["message"]) + + @patch("mailgun.client.requests.post") + def test_post_domain_creds(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Created"}) + request = self.client.domains_credentials.create( + domain=self.domain, data=self.post_domain_creds + ) + self.assertEqual(request.status_code, 200) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + @patch("mailgun.client.requests.put") + def test_update_simple_domain( + self, m_put: MagicMock, m_post: MagicMock, m_delete: MagicMock + ) -> None: + m_delete.return_value = mock_response(200) + m_post.return_value = mock_response(200, {"domain": {}}) + m_put.return_value = mock_response(200, {"message": "Domain has been updated"}) + self.client.domains.create(data=self.post_domain_data) + request = self.client.domains.put( + data={"spam_action": "disabled"}, domain=self.post_domain_data["name"] + ) + self.assertEqual(request.status_code, 200) + self.assertEqual(request.json()["message"], "Domain has been updated") + + @patch("mailgun.client.requests.post") + @patch("mailgun.client.requests.put") + def test_put_domain_creds(self, m_put: MagicMock, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Created"}) + m_put.return_value = mock_response(200, {"message": "Updated"}) + self.client.domains_credentials.create( + domain=self.domain, data=self.post_domain_creds + ) + request = self.client.domains_credentials.put( + domain=self.domain, data=self.put_domain_creds, login="alice_bob" + ) + self.assertEqual(request.status_code, 200) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.post") + @patch("mailgun.client.requests.put") + def test_put_mailboxes_credentials(self, m_put: MagicMock, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response( + 200, + { + "message": "Password changed", + "note": "", + "credentials": {f"alice_bob@{self.domain}": {}}, + }, + ) + self.client.domains_credentials.create( + domain=self.domain, data=self.post_domain_creds + ) + req = self.client.mailboxes.put( + domain=self.domain, login=f"alice_bob@{self.domain}" + ) + self.assertEqual(req.status_code, 200) + self.assertIn("Password changed", req.json()["message"]) + + @patch("mailgun.client.requests.get") + def test_get_domain_list(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.domainlist.get() + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.get") + def test_get_smtp_creds(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + request = self.client.domains_credentials.get(domain=self.domain) + self.assertEqual(request.status_code, 200) + self.assertIn("items", request.json()) + + @patch("mailgun.client.requests.get") + def test_get_sending_queues(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"scheduled": [], "retry": []}) + request = self.client.domains_sendingqueues.get(domain=self.test_domain) + self.assertEqual(request.status_code, 200) + self.assertIn("scheduled", request.json()) + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_get_single_domain(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_get.return_value = mock_response(200, {"domain": {"name": self.test_domain}}) + self.client.domains.create(data=self.post_domain_data) + req = self.client.domains.get(domain_name=self.post_domain_data["name"]) + self.assertEqual(req.status_code, 200) + self.assertIn("domain", req.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_verify_domain(self, m_post: MagicMock, m_put: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response(200, {"domain": {"state": "verified"}}) + self.client.domains.create(data=self.post_domain_data) + req = self.client.domains.put(domain=self.post_domain_data["name"], verify=True) + self.assertEqual(req.status_code, 200) + self.assertIn("domain", req.json()) + + @patch("mailgun.client.requests.put") + def test_put_domain_connections(self, m_put: MagicMock) -> None: + m_put.return_value = mock_response(200, {"message": "Updated"}) + request = self.client.domains_connection.put( + domain=self.domain, data=self.put_domain_connections_data + ) + self.assertEqual(request.status_code, 200) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.put") + def test_put_domain_tracking_open(self, m_put: MagicMock) -> None: + m_put.return_value = mock_response(200, {"message": "Updated"}) + request = self.client.domains_tracking_open.put( + domain=self.domain, data=self.put_domain_tracking_data + ) + self.assertEqual(request.status_code, 200) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.put") + def test_put_domain_tracking_click(self, m_put: MagicMock) -> None: + m_put.return_value = mock_response(200, {"message": "Updated"}) + request = self.client.domains_tracking_click.put( + domain=self.domain, data=self.put_domain_tracking_data + ) + self.assertEqual(request.status_code, 200) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.put") + def test_put_domain_unsubscribe(self, m_put: MagicMock) -> None: + m_put.return_value = mock_response(200, {"message": "Updated"}) + request = self.client.domains_tracking_unsubscribe.put( + domain=self.domain, data=self.put_domain_unsubscribe_data + ) + self.assertEqual(request.status_code, 200) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_put_dkim_authority(self, m_post: MagicMock, m_put: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response(200, {"message": "Updated"}) + self.client.domains.create(data=self.post_domain_data) + request = self.client.domains_dkimauthority.put( + domain=self.test_domain, data=self.put_domain_dkim_authority_data + ) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_put_webprefix(self, m_post: MagicMock, m_put: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response(200, {"message": "Updated"}) + self.client.domains.create(data=self.post_domain_data) + request = self.client.domains_webprefix.put( + domain=self.test_domain, data=self.put_domain_webprefix_data + ) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.put") + def test_put_dkim_selector(self, m_put: MagicMock) -> None: + m_put.return_value = mock_response(200, {"message": "Updated"}) + request = self.client.domains_dkimselector.put( + domain=self.domain, data=self.put_dkim_selector_data + ) + self.assertIn("message", request.json()) + + @patch("mailgun.client.requests.get") + def test_get_dkim_keys(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response( + 200, + { + "items": [ + { + "signing_domain": "python.test.domain5", + "selector": "smtp", + "dns_record": {}, + } + ], + "paging": {}, + }, + ) + data = { + "page": "string", + "limit": "0", + "signing_domain": "python.test.domain5", + "selector": "smtp", + } + req = self.client.dkim_keys.get(data=data) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + self.assertIn("paging", req.json()) + + @patch("mailgun.client.requests.delete") + def test_delete_dkim_keys(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "success"}) + query = {"signing_domain": "python.test.domain5", "selector": "smtp"} + req = self.client.dkim_keys.delete(filters=query) + self.assertEqual(req.status_code, 200) + self.assertIn("success", req.json()["message"]) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_delete_domain_creds(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response(200) + self.client.domains_credentials.create( + domain=self.domain, data=self.post_domain_creds + ) + request = self.client.domains_credentials.delete( + domain=self.domain, login="alice_bob" + ) + self.assertEqual(request.status_code, 200) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_delete_all_domain_credentials( + self, m_post: MagicMock, m_delete: MagicMock + ) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response( + 200, {"message": "All domain credentials have been deleted"} + ) + self.client.domains_credentials.create( + domain=self.domain, data=self.post_domain_creds + ) + request = self.client.domains_credentials.delete(domain=self.domain) + self.assertEqual(request.status_code, 200) + self.assertIn( + request.json()["message"], "All domain credentials have been deleted" + ) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_delete_domain(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response( + 200, {"message": "Domain will be deleted in the background"} + ) + self.client.domains.create(data=self.post_domain_data) + request = self.client.domains.delete(domain=self.test_domain) + self.assertEqual( + request.json()["message"], "Domain will be deleted in the background" + ) + self.assertEqual(request.status_code, 200) From 93490d3f3633148aff367761d7ceab5cfac5f72e Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Mon, 12 Jan 2026 10:51:21 +0100 Subject: [PATCH 05/30] Ported IpTests --- tests/unit/test_integration_mirror.py | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 6f1b783..e6ed9ca 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -334,3 +334,45 @@ def test_delete_domain(self, m_post: MagicMock, m_delete: MagicMock) -> None: request.json()["message"], "Domain will be deleted in the background" ) self.assertEqual(request.status_code, 200) + + +class IpTests(unittest.TestCase): + """Mirror of integration IpTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.ip_data = {"ip": "1.2.3.4"} + + @patch("mailgun.client.requests.get") + def test_get_ip_from_domain(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.ips.get(domain=self.domain, params={"dedicated": "true"}) + self.assertIn("items", req.json()) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_get_ip_by_address(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_get.return_value = mock_response(200, {"ip": self.ip_data["ip"]}) + self.client.domains_ips.create(domain=self.domain, data=self.ip_data) + req = self.client.ips.get(domain=self.domain, ip=self.ip_data["ip"]) + self.assertIn("ip", req.json()) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.post") + def test_create_ip(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "success"}) + request = self.client.domains_ips.create(domain=self.domain, data=self.ip_data) + self.assertEqual("success", request.json()["message"]) + self.assertEqual(request.status_code, 200) + + @patch("mailgun.client.requests.delete") + def test_delete_ip(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "success"}) + request = self.client.domains_ips.delete( + domain=self.domain, ip=self.ip_data["ip"] + ) + self.assertEqual("success", request.json()["message"]) + self.assertEqual(request.status_code, 200) From 170d756074ca0da56149b8e4663cceda0673309a Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Mon, 12 Jan 2026 17:36:15 +0100 Subject: [PATCH 06/30] Ported IpPoolTests --- tests/unit/test_integration_mirror.py | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index e6ed9ca..8ee667c 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -376,3 +376,54 @@ def test_delete_ip(self, m_delete: MagicMock) -> None: ) self.assertEqual("success", request.json()["message"]) self.assertEqual(request.status_code, 200) + + +class IpPoolsTests(unittest.TestCase): + """Mirror of integration IpPoolsTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.data = {"name": "test_pool", "description": "Test", "add_ip": "1.2.3.4"} + self.patch_data = {"name": "test_pool1", "description": "Test1"} + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_get_ippools(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200, {"pool_id": "pid"}) + m_get.return_value = mock_response(200, {"ip_pools": []}) + self.client.ippools.create(domain=self.domain, data=self.data) + req = self.client.ippools.get(domain=self.domain) + self.assertIn("ip_pools", req.json()) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.patch") + @patch("mailgun.client.requests.post") + def test_patch_ippool(self, m_post: MagicMock, m_patch: MagicMock) -> None: + m_post.return_value = mock_response(200, {"pool_id": "pid123"}) + m_patch.return_value = mock_response(200, {"message": "success"}) + self.client.ippools.create(domain=self.domain, data=self.data) + req = self.client.ippools.patch( + domain=self.domain, data=self.patch_data, pool_id="pid123" + ) + self.assertEqual("success", req.json()["message"]) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.post") + def test_link_domain_ippool(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Linked"}) + req = self.client.domains_ips.create( + domain=self.domain, data={"pool_id": "pid123"} + ) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_delete_ippool(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200, {"pool_id": "pid123"}) + m_delete.return_value = mock_response(200, {"message": "started"}) + self.client.ippools.create(domain=self.domain, data=self.data) + req_del = self.client.ippools.delete( + domain=self.domain, pool_id="pid123" + ) + self.assertEqual("started", req_del.json()["message"]) From 7a7cb67233f78d701e5e139ae6a8b9725a134089 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Tue, 20 Jan 2026 09:18:01 +0100 Subject: [PATCH 07/30] Ported EventsTests --- tests/unit/test_integration_mirror.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 8ee667c..d294e82 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -427,3 +427,26 @@ def test_delete_ippool(self, m_post: MagicMock, m_delete: MagicMock) -> None: domain=self.domain, pool_id="pid123" ) self.assertEqual("started", req_del.json()["message"]) + + +class EventsTests(unittest.TestCase): + """Mirror of integration EventsTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.params = {"event": "rejected"} + + @patch("mailgun.client.requests.get") + def test_events_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.events.get(domain=self.domain) + self.assertIn("items", req.json()) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.get") + def test_event_params(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.events.get(domain=self.domain, filters=self.params) + self.assertIn("items", req.json()) + self.assertEqual(req.status_code, 200) From 2953f9b6475a982e5fb3375db5bc37e60878504b Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 21 Jan 2026 09:28:55 +0100 Subject: [PATCH 08/30] Ported EventsTests --- tests/unit/test_integration_mirror.py | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index d294e82..95adf50 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -450,3 +450,71 @@ def test_event_params(self, m_get: MagicMock) -> None: req = self.client.events.get(domain=self.domain, filters=self.params) self.assertIn("items", req.json()) self.assertEqual(req.status_code, 200) + + +class TagsTests(unittest.TestCase): + """Mirror of integration TagsTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.data = {"description": "Tests running"} + self.put_tags_data = {"description": "Python testtt"} + self.stats_params = {"event": "accepted"} + self.tag_name = "Python test" + + @patch("mailgun.client.requests.get") + def test_get_tags(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.tags.get(domain=self.domain) + self.assertIn("items", req.json()) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.get") + def test_tag_get_by_name(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"tag": {"name": self.tag_name}}) + req = self.client.tags.get(domain=self.domain, tag_name=self.tag_name) + self.assertIn("tag", req.json()) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.put") + def test_tag_put(self, m_put: MagicMock) -> None: + m_put.return_value = mock_response(200, {"message": "Updated"}) + req = self.client.tags.put( + domain=self.domain, + tag_name=self.tag_name, + data=self.put_tags_data, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.get") + def test_tags_stats_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"tag": {}}) + req = self.client.tags_stats.get( + domain=self.domain, + filters=self.stats_params, + tag_name=self.tag_name, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("tag", req.json()) + + @patch("mailgun.client.requests.get") + def test_tags_stats_aggregate_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"tag": {}}) + req = self.client.tags_stats_aggregates_devices.get( + domain=self.domain, + filters=self.stats_params, + tag_name=self.tag_name, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("tag", req.json()) + + @patch("mailgun.client.requests.delete") + def test_delete_tags(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req = self.client.tags.delete( + domain=self.domain, tag_name=self.tag_name + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) From 1d3fd3211983697de33a0cd6bd432a8d2bbd916c Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Thu, 22 Jan 2026 10:23:01 +0100 Subject: [PATCH 09/30] Ported BouncesTests --- tests/unit/test_integration_mirror.py | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 95adf50..3e1a480 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -6,6 +6,7 @@ from __future__ import annotations +import json import unittest from unittest.mock import MagicMock, patch @@ -518,3 +519,83 @@ def test_delete_tags(self, m_delete: MagicMock) -> None: ) self.assertEqual(req.status_code, 200) self.assertIn("message", req.json()) + + +class BouncesTests(unittest.TestCase): + """Mirror of integration BouncesTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.bounces_data = { + "address": "test30@gmail.com", + "code": 550, + "error": "Test error", + } + self.bounces_json_data = """[{ + "address": "test121@i.ua", + "code": "550", + "error": "Test error2312" + }, { + "address": "test122@gmail.com", + "code": "550", + "error": "Test error" + }]""" + + @patch("mailgun.client.requests.get") + def test_bounces_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.bounces.get(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.post") + def test_bounces_create(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"address": self.bounces_data["address"]}) + req = self.client.bounces.create(data=self.bounces_data, domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("address", req.json()) + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_bounces_get_address(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_get.return_value = mock_response(200, {"address": self.bounces_data["address"]}) + self.client.bounces.create(data=self.bounces_data, domain=self.domain) + req = self.client.bounces.get( + domain=self.domain, bounce_address=self.bounces_data["address"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("address", req.json()) + + @patch("mailgun.client.requests.post") + def test_bounces_create_json(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Added"}) + json_data = json.loads(self.bounces_json_data) + for address in json_data: + req = self.client.bounces.create( + data=address, + domain=self.domain, + headers={"Content-type": "application/json"}, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_bounces_delete_single(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + self.client.bounces.create(data=self.bounces_data, domain=self.domain) + req = self.client.bounces.delete( + domain=self.domain, bounce_address=self.bounces_data["address"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.delete") + def test_bounces_delete_all(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req = self.client.bounces.delete(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) From 83cfc784443a4a53795c2988e8bb1580e4bb8792 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Fri, 30 Jan 2026 09:33:05 +0100 Subject: [PATCH 10/30] Ported UnsubscribesTests --- tests/unit/test_integration_mirror.py | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 3e1a480..ab3b6c7 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -599,3 +599,66 @@ def test_bounces_delete_all(self, m_delete: MagicMock) -> None: req = self.client.bounces.delete(domain=self.domain) self.assertEqual(req.status_code, 200) self.assertIn("message", req.json()) + + +class UnsubscribesTests(unittest.TestCase): + """Mirror of integration UnsubscribesTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.unsub_data = {"address": "test@gmail.com", "tag": "unsub_test_tag"} + self.unsub_json_data = """[{"address": "test1@gmail.com", "tags": ["some tag"]}, + {"address": "test2@gmail.com", "code": ["*"]}, {"address": "test3@gmail.com"}]""" + + @patch("mailgun.client.requests.post") + def test_unsub_create(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Added"}) + req = self.client.unsubscribes.create(data=self.unsub_data, domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.get") + def test_unsub_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.unsubscribes.get(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.get") + def test_unsub_get_single(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"address": self.unsub_data["address"]}) + req = self.client.unsubscribes.get( + domain=self.domain, unsubscribe_address=self.unsub_data["address"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("address", req.json()) + + @patch("mailgun.client.requests.post") + def test_unsub_create_multiple(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Added"}) + json_data = json.loads(self.unsub_json_data) + for address in json_data: + req = self.client.unsubscribes.create( + data=address, + domain=self.domain, + headers={"Content-type": "application/json"}, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.delete") + def test_unsub_delete(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req = self.client.unsubscribes.delete( + domain=self.domain, unsubscribe_address=self.unsub_data["address"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.delete") + def test_unsub_delete_all(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req = self.client.unsubscribes.delete(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) From c748c6b96b9146f2b5c437efb0cdcbb72c508bb2 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Sat, 31 Jan 2026 22:08:32 +0100 Subject: [PATCH 11/30] Ported ComplaintsTests --- tests/unit/test_integration_mirror.py | 75 +++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index ab3b6c7..c7ba834 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -662,3 +662,78 @@ def test_unsub_delete_all(self, m_delete: MagicMock) -> None: req = self.client.unsubscribes.delete(domain=self.domain) self.assertEqual(req.status_code, 200) self.assertIn("message", req.json()) + + +class ComplaintsTests(unittest.TestCase): + """Mirror of integration ComplaintsTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.compl_data = {"address": "test@gmail.com", "tag": "compl_test_tag"} + self.compl_json_data = """[{"address": "test1@gmail.com", "tags": ["some tag"]}, + {"address": "test3@gmail.com"}]""" + + @patch("mailgun.client.requests.post") + def test_compl_create(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Added"}) + req = self.client.complaints.create(data=self.compl_data, domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.get") + def test_get_single_complaint(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.complaints.get(data=self.compl_data, domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.get") + def test_compl_get_all(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.complaints.get(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_compl_get_single(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_get.return_value = mock_response(200, {"address": self.compl_data["address"]}) + self.client.complaints.create(data=self.compl_data, domain=self.domain) + req = self.client.complaints.get( + domain=self.domain, complaint_address=self.compl_data["address"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("address", req.json()) + + @patch("mailgun.client.requests.post") + def test_compl_create_multiple(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Added"}) + json_data = json.loads(self.compl_json_data) + for address in json_data: + req = self.client.complaints.create( + data=address, + domain=self.domain, + headers={"Content-type": "application/json"}, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_compl_delete_single(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req = self.client.complaints.delete( + domain=self.domain, unsubscribe_address=self.compl_data["address"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.delete") + def test_compl_delete_all(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req = self.client.complaints.delete(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) From 46026a40fc59e04a7fef013904868a9d1c7fcd41 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Mon, 9 Feb 2026 09:10:59 +0100 Subject: [PATCH 12/30] Ported WhiteListTests --- tests/unit/test_integration_mirror.py | 32 +++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index c7ba834..67e6aed 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -737,3 +737,35 @@ def test_compl_delete_all(self, m_delete: MagicMock) -> None: req = self.client.complaints.delete(domain=self.domain) self.assertEqual(req.status_code, 200) self.assertIn("message", req.json()) + + +class WhiteListTests(unittest.TestCase): + """Mirror of integration WhiteListTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.whitel_data = {"address": "test@gmail.com"} + + @patch("mailgun.client.requests.post") + def test_whitel_create(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Added"}) + req = self.client.whitelists.create(data=self.whitel_data, domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.get") + def test_whitel_get_simple(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.whitelists.get(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.delete") + def test_whitel_delete_simple(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req = self.client.whitelists.delete( + domain=self.domain, whitelist_address=self.whitel_data["address"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) From 472a3395cc242a6b03117f1a270dad9524a7a91c Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Mon, 9 Feb 2026 09:59:11 +0100 Subject: [PATCH 13/30] Ported RoutesTests --- tests/unit/test_integration_mirror.py | 112 ++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 67e6aed..59149e3 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -769,3 +769,115 @@ def test_whitel_delete_simple(self, m_delete: MagicMock) -> None: ) self.assertEqual(req.status_code, 200) self.assertIn("message", req.json()) + + +class RoutesTests(unittest.TestCase): + """Mirror of integration RoutesTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.sender = "sender@example.com" + self.routes_data = { + "priority": 0, + "description": "Sample route", + "expression": f"match_recipient('.*@{self.domain}')", + "action": ["forward('http://myhost.com/messages/')", "stop()"], + } + self.routes_params = {"skip": 1, "limit": 1} + self.routes_put_data = {"priority": 2} + + @patch("mailgun.client.requests.post") + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.get") + def test_routes_create( + self, m_get: MagicMock, m_delete: MagicMock, m_post: MagicMock + ) -> None: + m_get.return_value = mock_response(200, {"items": [{"id": "rid"}]}) + m_delete.return_value = mock_response(200) + m_post.return_value = mock_response(200, {"message": "Route created"}) + params = {"skip": 0, "limit": 1} + req1 = self.client.routes.get(domain=self.domain, filters=params) + if req1.json().get("items"): + self.client.routes.delete( + domain=self.domain, route_id=req1.json()["items"][0]["id"] + ) + req = self.client.routes.create(domain=self.domain, data=self.routes_data) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.get") + def test_routes_get_all(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.routes.get(domain=self.domain, filters=self.routes_params) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_get_route_by_id(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200, {"route": {"id": "rid123"}}) + m_get.return_value = mock_response(200, {"route": {"id": "rid123"}}) + req_post = self.client.routes.create( + domain=self.domain, data=self.routes_data + ) + req = self.client.routes.get( + domain=self.domain, route_id=req_post.json()["route"]["id"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("route", req.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_routes_put(self, m_post: MagicMock, m_put: MagicMock) -> None: + m_post.return_value = mock_response(200, {"route": {"id": "rid123"}}) + m_put.return_value = mock_response(200, {"message": "Updated"}) + req_post = self.client.routes.create( + domain=self.domain, data=self.routes_data + ) + req = self.client.routes.put( + domain=self.domain, + data=self.routes_put_data, + route_id=req_post.json()["route"]["id"], + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_routes_delete(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200, {"route": {"id": "rid123"}}) + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req_post = self.client.routes.create( + domain=self.domain, data=self.routes_data + ) + req = self.client.routes.delete( + domain=self.domain, route_id=req_post.json()["route"]["id"] + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.get") + def test_get_routes_match(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response( + 200, + { + "route": { + "actions": [], + "created_at": "", + "description": "", + "expression": "", + "id": "r1", + "priority": 0, + } + }, + ) + query = {"address": self.sender} + req = self.client.routes_match.get(domain=self.domain, filters=query) + self.assertEqual(req.status_code, 200) + self.assertIn("route", req.json()) + expected_keys = [ + "actions", "created_at", "description", "expression", "id", "priority" + ] + for key in expected_keys: + self.assertIn(key, req.json()["route"]) From ef8430d39a4724a7d0e137e36522275a5d2ad6d9 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 11 Feb 2026 09:33:34 +0100 Subject: [PATCH 14/30] Ported WebhooksTests --- tests/unit/test_integration_mirror.py | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 59149e3..6671739 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -881,3 +881,72 @@ def test_get_routes_match(self, m_get: MagicMock) -> None: ] for key in expected_keys: self.assertIn(key, req.json()["route"]) + + +class WebhooksTests(unittest.TestCase): + """Mirror of integration WebhooksTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.webhooks_data = {"id": "clicked", "url": ["https://i.ua"]} + self.webhooks_data_put = {"url": "https://twitter.com"} + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_webhooks_create(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Created"}) + m_delete.return_value = mock_response(200) + req = self.client.domains_webhooks.create( + domain=self.domain, data=self.webhooks_data + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + self.client.domains_webhooks_clicked.delete(domain=self.domain) + + @patch("mailgun.client.requests.get") + def test_webhooks_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"webhooks": {}}) + req = self.client.domains_webhooks.get(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("webhooks", req.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_webhook_put(self, m_post: MagicMock, m_put: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response(200, {"message": "Updated"}) + self.client.domains_webhooks.create( + domain=self.domain, data=self.webhooks_data + ) + req = self.client.domains_webhooks_clicked.put( + domain=self.domain, data=self.webhooks_data_put + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) + self.client.domains_webhooks_clicked.delete(domain=self.domain) + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_webhook_get_simple(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_get.return_value = mock_response(200, {"webhook": {}}) + self.client.domains_webhooks.create( + domain=self.domain, data=self.webhooks_data + ) + req = self.client.domains_webhooks_clicked.get(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("webhook", req.json()) + self.client.domains_webhooks_clicked.delete(domain=self.domain) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_webhook_delete(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + self.client.domains_webhooks.create( + domain=self.domain, data=self.webhooks_data + ) + req = self.client.domains_webhooks_clicked.delete(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) From 4a2740a2f033b8520996087fa2e0c9375cc54a7b Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 11 Feb 2026 09:41:31 +0100 Subject: [PATCH 15/30] Ported MailingListsTests --- tests/unit/test_integration_mirror.py | 170 ++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 6671739..cff0e52 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -950,3 +950,173 @@ def test_webhook_delete(self, m_post: MagicMock, m_delete: MagicMock) -> None: req = self.client.domains_webhooks_clicked.delete(domain=self.domain) self.assertEqual(req.status_code, 200) self.assertIn("message", req.json()) + + +class MailingListsTests(unittest.TestCase): + """Mirror of integration MailingListsTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.maillist_address = f"list@{self.domain}" + self.mailing_lists_data = { + "address": f"python_sdk@{self.domain}", + "description": "Mailgun developers list", + } + self.mailing_lists_data_update = {"description": "Mailgun developers list 121212"} + self.mailing_lists_members_data = { + "subscribed": True, + "address": "bar@example.com", + "name": "Bob Bar", + "description": "Developer", + "vars": '{"age": 26}', + } + self.mailing_lists_members_put_data = { + "subscribed": True, + "address": "bar@example.com", + "name": "Bob Bar", + "description": "Developer", + "vars": '{"age": 28}', + } + self.mailing_lists_members_data_mult = { + "upsert": True, + "members": '[{"address": "alice@example.com"}, {"address": "bob@example.com"}]', + } + + @patch("mailgun.client.requests.get") + def test_maillist_pages_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.lists_pages.get(domain=self.domain) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.get") + def test_maillist_lists_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"list": {}}) + req = self.client.lists.get( + domain=self.domain, address=self.maillist_address + ) + self.assertEqual(req.status_code, 200) + self.assertIn("list", req.json()) + + @patch("mailgun.client.requests.post") + def test_maillist_lists_create(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"list": {}}) + req = self.client.lists.create( + domain=self.domain, data=self.mailing_lists_data + ) + self.assertEqual(req.status_code, 200) + self.assertIn("list", req.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_maillists_lists_put(self, m_post: MagicMock, m_put: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response(200, {"list": {}}) + self.client.lists.create(domain=self.domain, data=self.mailing_lists_data) + req = self.client.lists.put( + domain=self.domain, + data=self.mailing_lists_data_update, + address=f"python_sdk@{self.domain}", + ) + self.assertEqual(req.status_code, 200) + self.assertIn("list", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_maillists_lists_delete( + self, m_post: MagicMock, m_delete: MagicMock + ) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response(200) + self.client.lists.create(domain=self.domain, data=self.mailing_lists_data) + req = self.client.lists.delete( + domain=self.domain, address=f"python_sdk@{self.domain}" + ) + self.assertEqual(req.status_code, 200) + self.client.lists.create(domain=self.domain, data=self.mailing_lists_data) + + @patch("mailgun.client.requests.get") + def test_maillists_lists_members_pages_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.lists_members_pages.get( + domain=self.domain, address=self.maillist_address + ) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.post") + @patch("mailgun.client.requests.delete") + def test_maillists_lists_members_create( + self, m_delete: MagicMock, m_post: MagicMock + ) -> None: + m_delete.return_value = mock_response(200) + m_post.return_value = mock_response(200, {"member": {}}) + req = self.client.lists_members.create( + domain=self.domain, + address=self.maillist_address, + data=self.mailing_lists_members_data, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("member", req.json()) + + @patch("mailgun.client.requests.get") + def test_maillists_lists_members_get(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"items": []}) + req = self.client.lists_members.get( + domain=self.domain, address=self.maillist_address + ) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_maillists_lists_members_update( + self, m_post: MagicMock, m_put: MagicMock + ) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response(200, {"member": {}}) + self.client.lists_members.create( + domain=self.domain, + address=self.maillist_address, + data=self.mailing_lists_members_data, + ) + req = self.client.lists_members.put( + domain=self.domain, + address=self.maillist_address, + data=self.mailing_lists_members_put_data, + member_address=self.mailing_lists_members_data["address"], + ) + self.assertEqual(req.status_code, 200) + self.assertIn("member", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_maillists_lists_members_delete( + self, m_post: MagicMock, m_delete: MagicMock + ) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response(200) + self.client.lists_members.create( + domain=self.domain, + address=self.maillist_address, + data=self.mailing_lists_members_data, + ) + req = self.client.lists_members.delete( + domain=self.domain, + address=self.maillist_address, + member_address=self.mailing_lists_members_data["address"], + ) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.post") + def test_maillists_lists_members_create_mult(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"message": "Added"}) + req = self.client.lists_members.create( + domain=self.domain, + address=self.maillist_address, + data=self.mailing_lists_members_data_mult, + multiple=True, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("message", req.json()) From a88fa0778056ee4c493aa57e5b9cbfce24522f50 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Thu, 12 Feb 2026 09:20:57 +0100 Subject: [PATCH 16/30] Ported TemplatesTests --- tests/unit/test_integration_mirror.py | 178 ++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index cff0e52..3cb9ea9 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -1120,3 +1120,181 @@ def test_maillists_lists_members_create_mult(self, m_post: MagicMock) -> None: ) self.assertEqual(req.status_code, 200) self.assertIn("message", req.json()) + + +class TemplatesTests(unittest.TestCase): + """Mirror of integration TemplatesTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.post_template_data = { + "name": "template.name20", + "description": "template description", + "template": "{{fname}} {{lname}}", + "engine": "handlebars", + "comment": "version comment", + } + self.put_template_data = {"description": "new template description"} + self.post_template_version_data = { + "tag": "v11", + "template": "{{fname}} {{lname}}", + "engine": "handlebars", + "active": "no", + } + self.put_template_version_data = { + "template": "{{fname}} {{lname}}", + "comment": "Updated version comment", + "active": "no", + } + self.put_template_version = "v11" + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_create_template(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200) + m_post.return_value = mock_response(200, {"template": {}}) + self.client.templates.delete( + domain=self.domain, + template_name=self.post_template_data["name"], + ) + req = self.client.templates.create( + data=self.post_template_data, domain=self.domain + ) + self.assertEqual(req.status_code, 200) + self.assertIn("template", req.json()) + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_get_template(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_get.return_value = mock_response(200, {"template": {}}) + params = {"active": "yes"} + req = self.client.templates.get( + domain=self.domain, + filters=params, + template_name=self.post_template_data["name"], + ) + self.assertEqual(req.status_code, 200) + self.assertIn("template", req.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_put_template(self, m_post: MagicMock, m_put: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response(200, {"template": {}}) + self.client.templates.create( + data=self.post_template_data, domain=self.domain + ) + req = self.client.templates.put( + domain=self.domain, + data=self.put_template_data, + template_name=self.post_template_data["name"], + ) + self.assertEqual(req.status_code, 200) + self.assertIn("template", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_delete_template(self, m_post: MagicMock, m_delete: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response(200) + self.client.templates.create( + data=self.post_template_data, domain=self.domain + ) + req = self.client.templates.delete( + domain=self.domain, + template_name=self.post_template_data["name"], + ) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.post") + def test_post_version_template(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"template": {}}) + req = self.client.templates.create( + data=self.post_template_version_data, + domain=self.domain, + template_name=self.post_template_data["name"], + versions=True, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("template", req.json()) + + @patch("mailgun.client.requests.get") + @patch("mailgun.client.requests.post") + def test_get_version_template(self, m_post: MagicMock, m_get: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_get.return_value = mock_response(200, {"template": {}}) + req = self.client.templates.get( + domain=self.domain, + template_name=self.post_template_data["name"], + versions=True, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("template", req.json()) + + @patch("mailgun.client.requests.put") + @patch("mailgun.client.requests.post") + def test_put_version_template(self, m_post: MagicMock, m_put: MagicMock) -> None: + m_post.return_value = mock_response(200) + m_put.return_value = mock_response(200, {"template": {}}) + req = self.client.templates.put( + domain=self.domain, + data=self.put_template_version_data, + template_name=self.post_template_data["name"], + versions=True, + tag=self.put_template_version, + ) + self.assertEqual(req.status_code, 200) + self.assertIn("template", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.post") + def test_delete_version_template( + self, m_post: MagicMock, m_delete: MagicMock + ) -> None: + m_post.return_value = mock_response(200) + m_delete.return_value = mock_response(200) + self.client.templates.create( + data=self.post_template_data, domain=self.domain + ) + req = self.client.templates.delete( + domain=self.domain, + template_name=self.post_template_data["name"], + versions=True, + tag="v0", + ) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.put") + def test_update_template_version_copy(self, m_put: MagicMock) -> None: + m_put.return_value = mock_response( + 200, + { + "message": "version has been copied", + "version": {"tag": "v3"}, + "template": { + "tag": "v3", + "template": "", + "engine": "handlebars", + "mjml": False, + "createdAt": "", + "comment": "", + "active": "no", + "id": "", + "headers": {}, + }, + }, + ) + data = {"comment": "An updated version comment"} + req = self.client.templates.put( + domain=self.domain, + filters=data, + template_name="template.name1", + versions=True, + tag="v2", + copy=True, + new_tag="v3", + ) + self.assertEqual(req.status_code, 200) + self.assertIn("version has been copied", req.json()["message"]) From c300b277dee147c7dd4007eeb3c3bc10eaa5768d Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Fri, 13 Feb 2026 20:15:15 +0100 Subject: [PATCH 17/30] Ported MetricsTests --- tests/unit/test_integration_mirror.py | 151 ++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 3cb9ea9..112c1d7 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -8,6 +8,7 @@ import json import unittest +from datetime import datetime, timedelta from unittest.mock import MagicMock, patch from mailgun.client import Client @@ -1298,3 +1299,153 @@ def test_update_template_version_copy(self, m_put: MagicMock) -> None: ) self.assertEqual(req.status_code, 200) self.assertIn("version has been copied", req.json()["message"]) + + +class MetricsTest(unittest.TestCase): + """Mirror of integration MetricsTest with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + now = datetime.now() + now_formatted = now.strftime("%a, %d %b %Y %H:%M:%S +0000") + yesterday = now - timedelta(days=1) + yesterday_formatted = yesterday.strftime("%a, %d %b %Y %H:%M:%S +0000") + self.account_metrics_data = { + "start": yesterday_formatted, + "end": now_formatted, + "resolution": "day", + "duration": "1m", + "dimensions": ["time"], + "metrics": ["accepted_count", "delivered_count", "clicked_rate", "opened_rate"], + "filter": { + "AND": [ + { + "attribute": "domain", + "comparator": "=", + "values": [{"label": self.domain, "value": self.domain}], + } + ] + }, + "include_subaccounts": True, + "include_aggregates": True, + } + self.invalid_account_metrics_data = { + **self.account_metrics_data, + "resolution": "century", + "duration": "1c", + } + self.account_usage_metrics_data = { + "start": yesterday_formatted, + "end": now_formatted, + "resolution": "day", + "duration": "1m", + "dimensions": ["time"], + "metrics": ["accessibility_count", "processed_count"], + "include_subaccounts": True, + "include_aggregates": True, + } + self.invalid_account_usage_metrics_data = { + **self.account_usage_metrics_data, + "resolution": "century", + } + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_metrics(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response( + 200, + { + "start": "", + "end": "", + "resolution": "day", + "duration": "1m", + "dimensions": [], + "pagination": {}, + "items": [{"metrics": {"delivered_count": 0}, "dimensions": {}}], + "aggregates": {}, + }, + ) + req = self.client.analytics_metrics.create(data=self.account_metrics_data) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + self.assertIn("delivered_count", req.json()["items"][0]["metrics"]) + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_metrics_invalid_data( + self, m_post: MagicMock + ) -> None: + m_post.return_value = mock_response( + 400, {"message": "'resolution' attribute is invalid"} + ) + req = self.client.analytics_metrics.create( + data=self.invalid_account_metrics_data + ) + self.assertEqual(req.status_code, 400) + self.assertIn("'resolution' attribute is invalid", req.json()["message"]) + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_metrics_invalid_url( + self, m_post: MagicMock + ) -> None: + m_post.return_value = mock_response(404, {}) + req = self.client.analytics_metric.create(data=self.account_metrics_data) + self.assertEqual(req.status_code, 404) + + def test_post_query_get_account_metrics_invalid_url_without_underscore( + self, + ) -> None: + with self.assertRaises(KeyError) as cm: + self.client.analyticsmetric.create(data=self.account_metrics_data) + self.assertEqual(str(cm.exception), "'analyticsmetric'") + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_usage_metrics(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response( + 200, + { + "start": "", + "end": "", + "resolution": "day", + "duration": "1m", + "dimensions": [], + "pagination": {}, + "items": [{"metrics": {"email_validation_count": 0}, "dimensions": {}}], + "aggregates": {}, + }, + ) + req = self.client.analytics_usage_metrics.create( + data=self.account_usage_metrics_data + ) + self.assertEqual(req.status_code, 200) + self.assertIn("email_validation_count", req.json()["items"][0]["metrics"]) + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_usage_metrics_invalid_data( + self, m_post: MagicMock + ) -> None: + m_post.return_value = mock_response( + 400, {"message": "'resolution' attribute is invalid"} + ) + req = self.client.analytics_usage_metrics.create( + data=self.invalid_account_usage_metrics_data + ) + self.assertEqual(req.status_code, 400) + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_usage_metrics_invalid_url( + self, m_post: MagicMock + ) -> None: + m_post.return_value = mock_response(404, {}) + req = self.client.analytics_usage_metric.create( + data=self.invalid_account_usage_metrics_data + ) + self.assertEqual(req.status_code, 404) + + def test_post_query_get_account_usage_metrics_invalid_url_without_underscore( + self, + ) -> None: + with self.assertRaises(KeyError) as cm: + self.client.analyticsusagemetrics.create( + data=json.dumps(self.invalid_account_usage_metrics_data) + ) + self.assertEqual(str(cm.exception), "'analyticsusagemetrics'") From 5318a51866fed9d245cff278db06ba2da07df6ef Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 18 Feb 2026 10:08:17 +0100 Subject: [PATCH 18/30] Ported LogsTests --- tests/unit/test_integration_mirror.py | 91 +++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 112c1d7..e6358fd 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -1449,3 +1449,94 @@ def test_post_query_get_account_usage_metrics_invalid_url_without_underscore( data=json.dumps(self.invalid_account_usage_metrics_data) ) self.assertEqual(str(cm.exception), "'analyticsusagemetrics'") + + +class LogsTests(unittest.TestCase): + """Mirror of integration LogsTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + now = datetime.now() + now_formatted = now.strftime("%a, %d %b %Y %H:%M:%S +0000") + yesterday = now - timedelta(days=1) + yesterday_formatted = yesterday.strftime("%a, %d %b %Y %H:%M:%S +0000") + self.account_logs_data = { + "start": yesterday_formatted, + "end": now_formatted, + "filter": { + "AND": [ + { + "attribute": "domain", + "comparator": "=", + "values": [{"label": self.domain, "value": self.domain}], + } + ] + }, + "include_subaccounts": True, + "pagination": {"sort": "timestamp:asc", "limit": 50}, + } + self.invalid_account_logs_data = { + "start": yesterday_formatted, + "end": now_formatted, + "filter": { + "AND": [ + { + "attribute": "test", + "comparator": "=", + "values": [{"label": "", "value": ""}], + } + ] + }, + "include_subaccounts": True, + "pagination": {"sort": "timestamp:asc", "limit": 0}, + } + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_logs(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response( + 200, + { + "start": "", + "end": "", + "pagination": {}, + "items": [{"event": "", "account": ""}], + "aggregates": {}, + }, + ) + req = self.client.analytics_logs.create(data=self.account_logs_data) + self.assertEqual(req.status_code, 200) + self.assertIn("items", req.json()) + self.assertIn("event", req.json()["items"][0]) + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_logs_invalid_data( + self, m_post: MagicMock + ) -> None: + m_post.return_value = mock_response( + 400, + {"message": "'test' is not a valid filter predicate attribute"}, + ) + req = self.client.analytics_logs.create( + data=self.invalid_account_logs_data + ) + self.assertEqual(req.status_code, 400) + self.assertIn( + "'test' is not a valid filter predicate attribute", + req.json()["message"], + ) + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_logs_invalid_url( + self, m_post: MagicMock + ) -> None: + m_post.return_value = mock_response(404, {}) + req = self.client.analytics_log.create(data=self.account_logs_data) + self.assertEqual(req.status_code, 404) + + def test_post_query_get_account_logs_invalid_url_without_underscore( + self, + ) -> None: + with self.assertRaises(KeyError) as cm: + self.client.analyticslogs.create(data=self.account_logs_data) + self.assertEqual(str(cm.exception), "'analyticslogs'") From f5f2aa6ca238b0a103816d5475f14f7d8bf52a22 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 18 Feb 2026 10:24:31 +0100 Subject: [PATCH 19/30] Ported TagsNewTests --- tests/unit/test_integration_mirror.py | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index e6358fd..f0684a5 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -1540,3 +1540,71 @@ def test_post_query_get_account_logs_invalid_url_without_underscore( with self.assertRaises(KeyError) as cm: self.client.analyticslogs.create(data=self.account_logs_data) self.assertEqual(str(cm.exception), "'analyticslogs'") + + +class TagsNewTests(unittest.TestCase): + """Mirror of integration TagsNewTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.account_tags_data = { + "pagination": {"sort": "lastseen:desc", "limit": 10}, + "include_subaccounts": True, + } + self.account_tag_info = '{"tag": "Python test", "description": "updated"}' + self.account_tag_invalid_info = '{"tag": "test", "description": "updated"}' + + @patch("mailgun.client.requests.put") + def test_update_account_tag(self, m_put: MagicMock) -> None: + m_put.return_value = mock_response(200, {"message": "Updated"}) + req = self.client.analytics_tags.put(data=self.account_tag_info) + self.assertEqual(req.status_code, 200) + + def test_update_account_invalid_tag(self) -> None: + """Invalid endpoint name raises KeyError when building URL (handler lookup).""" + with self.assertRaises(KeyError) as cm: + self.client.nonexistent_endpoint.put(data=self.account_tag_invalid_info) + self.assertEqual(str(cm.exception), "'nonexistent'") + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_tags(self, m_post: MagicMock) -> None: + """Post query to list account tags (integration uses .create with data).""" + m_post.return_value = mock_response( + 200, + {"pagination": {"sort": "lastseen:desc", "limit": 10}, "items": []}, + ) + req = self.client.analytics_tags.create(data=self.account_tags_data) + self.assertEqual(req.status_code, 200) + self.assertIn("pagination", req.json()) + self.assertIn("items", req.json()) + self.assertIn("sort", req.json()["pagination"]) + self.assertIn("limit", req.json()["pagination"]) + + @patch("mailgun.client.requests.post") + def test_post_query_get_account_tags_with_incorrect_url( + self, m_post: MagicMock + ) -> None: + """Invalid URL (analytics_tag singular) returns 404.""" + m_post.return_value = mock_response(404, {"error": "Not found"}) + req = self.client.analytics_tag.create(data=self.account_tags_data) + self.assertEqual(req.status_code, 404) + self.assertIn("error", req.json()) + + @patch("mailgun.client.requests.delete") + def test_delete_account_tag(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(200, {"message": "Deleted"}) + req = self.client.analytics_tags.delete(tag_name="Python test") + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.delete") + def test_delete_account_nonexistent_tag(self, m_delete: MagicMock) -> None: + m_delete.return_value = mock_response(404, {"message": "Not found"}) + req = self.client.analytics_tags.delete(tag_name="nonexistent") + self.assertEqual(req.status_code, 404) + + @patch("mailgun.client.requests.get") + def test_get_account_tag_limit_information(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response(200, {"limits": {}}) + req = self.client.analytics_tags_limits.get() + self.assertEqual(req.status_code, 200) From 76341042aa54c5c7f006f0e99db0cf636090c04c Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 18 Feb 2026 10:41:56 +0100 Subject: [PATCH 20/30] Ported BounceClassificationTests --- tests/unit/test_integration_mirror.py | 46 +++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index f0684a5..f0acbad 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -1608,3 +1608,49 @@ def test_get_account_tag_limit_information(self, m_get: MagicMock) -> None: m_get.return_value = mock_response(200, {"limits": {}}) req = self.client.analytics_tags_limits.get() self.assertEqual(req.status_code, 200) + + +class BounceClassificationTests(unittest.TestCase): + """Mirror of integration BounceClassificationTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + + @patch("mailgun.client.requests.post") + def test_post_list_statistic(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(200, {"items": []}) + data = { + "dimensions": ["classification_id"], + "start": "2024-01-01", + "end": "2024-01-31", + } + req = self.client.analytics_bounce_classification_metrics.create(data=data) + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.post") + def test_post_list_statistic_without_dimensions(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(400, {"message": "dimensions required"}) + data = {"start": "2024-01-01", "end": "2024-01-31"} + req = self.client.analytics_bounce_classification_metrics.create(data=data) + self.assertEqual(req.status_code, 400) + + @patch("mailgun.client.requests.post") + def test_post_list_statistic_with_old_dates(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response( + 400, {"message": "is out of permitted log retention"} + ) + data = { + "dimensions": ["classification_id"], + "start": "2020-01-01", + "end": "2020-01-31", + } + req = self.client.analytics_bounce_classification_metrics.create(data=data) + self.assertEqual(req.status_code, 400) + self.assertIn("message", req.json()) + + @patch("mailgun.client.requests.post") + def test_post_list_statistic_with_empty_payload(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response(400, {"message": "Bad request"}) + req = self.client.analytics_bounce_classification_metrics.create(data={}) + self.assertEqual(req.status_code, 400) From 5f3b083c3944cc37d82c0ab5907135753043e2f2 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 18 Feb 2026 11:13:11 +0100 Subject: [PATCH 21/30] Ported UsersTests --- tests/unit/test_integration_mirror.py | 131 ++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index f0acbad..4dd25e5 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -1654,3 +1654,134 @@ def test_post_list_statistic_with_empty_payload(self, m_post: MagicMock) -> None m_post.return_value = mock_response(400, {"message": "Bad request"}) req = self.client.analytics_bounce_classification_metrics.create(data={}) self.assertEqual(req.status_code, 400) + + +class UsersTests(unittest.TestCase): + """Mirror of integration UsersTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.client_with_secret_key = Client(auth=("api", "fake-secret")) + self.domain = DOMAIN + self.mailgun_email = "user@example.com" + + @patch("mailgun.client.requests.get") + def test_get_users(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response( + 200, + { + "total": 1, + "users": [ + { + "account_id": "", + "activated": True, + "auth": {}, + "email": self.mailgun_email, + "id": "uid", + "role": "admin", + "name": "", + "is_disabled": False, + "is_master": False, + "metadata": {}, + "migration_status": "", + "email_details": {}, + "github_user_id": "", + "opened_ip": "", + "password_updated_at": "", + "preferences": {}, + "salesforce_user_id": "", + "tfa_active": False, + "tfa_created_at": "", + "tfa_enabled": False, + } + ], + }, + ) + query = {"role": "admin", "limit": "0", "skip": "0"} + req = self.client.users.get(filters=query) + self.assertEqual(req.status_code, 200) + self.assertIn("users", req.json()) + self.assertIn("total", req.json()) + + def test_get_user_invalid_url(self) -> None: + query = {"role": "admin", "limit": "0", "skip": "0"} + with self.assertRaises(KeyError): + self.client.user.get(filters=query) + + @patch("mailgun.client.requests.get") + def test_own_user_details(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response( + 200, + { + "account_id": "", + "activated": True, + "auth": {}, + "email": self.mailgun_email, + "id": "me", + "role": "admin", + "name": "", + "is_disabled": False, + "is_master": False, + "metadata": {}, + "migration_status": "", + "email_details": {}, + "github_user_id": "", + "opened_ip": "", + "password_updated_at": "", + "preferences": {}, + "salesforce_user_id": "", + "tfa_active": False, + "tfa_created_at": "", + "tfa_enabled": False, + }, + ) + req = self.client_with_secret_key.users.get(user_id="me") + self.assertEqual(req.status_code, 200) + + @patch("mailgun.client.requests.get") + def test_get_user_details(self, m_get: MagicMock) -> None: + user_obj = { + "account_id": "", + "activated": True, + "auth": {}, + "email": self.mailgun_email, + "id": "uid", + "role": "admin", + "name": "", + "is_disabled": False, + "is_master": False, + "metadata": {}, + "migration_status": "", + "email_details": {}, + "github_user_id": "", + "opened_ip": "", + "password_updated_at": "", + "preferences": {}, + "salesforce_user_id": "", + "tfa_active": False, + "tfa_created_at": "", + "tfa_enabled": False, + } + m_get.side_effect = [ + mock_response(200, {"users": [user_obj], "total": 1}), + mock_response(200, user_obj), + ] + query = {"role": "admin", "limit": "0", "skip": "0"} + req1 = self.client.users.get(filters=query) + user_id = req1.json()["users"][0]["id"] + req2 = self.client.users.get(user_id=user_id) + self.assertEqual(req2.status_code, 200) + + @patch("mailgun.client.requests.get") + def test_get_invalid_user_details(self, m_get: MagicMock) -> None: + m_get.side_effect = [ + mock_response(200, {"users": [{"id": "uid", "email": self.mailgun_email}], "total": 1}), + mock_response(404, {"message": "Not found"}), + ] + query = {"role": "admin", "limit": "0", "skip": "0"} + req1 = self.client.users.get(filters=query) + for user in req1.json()["users"]: + if self.mailgun_email == user["email"]: + req2 = self.client.users.get(user_id="xxxxxxx") + self.assertEqual(req2.status_code, 404) + break From b9202f41a73789c1be110d11a0f84775966149a3 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 18 Feb 2026 11:29:04 +0100 Subject: [PATCH 22/30] Ported KeysTests --- tests/unit/test_integration_mirror.py | 91 +++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 4dd25e5..98b7e1c 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -1785,3 +1785,94 @@ def test_get_invalid_user_details(self, m_get: MagicMock) -> None: req2 = self.client.users.get(user_id="xxxxxxx") self.assertEqual(req2.status_code, 404) break + + +class KeysTests(unittest.TestCase): + """Mirror of integration KeysTests with mocked HTTP.""" + + def setUp(self) -> None: + self.client = Client(auth=AUTH) + self.domain = DOMAIN + self.mailgun_email = "user@example.com" + self.role = "admin" + self.user_id = "uid" + self.user_name = "Test User" + + @patch("mailgun.client.requests.get") + def test_get_keys(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response( + 200, + {"total_count": 1, "items": [{"id": "key1", "requestor": self.mailgun_email}]}, + ) + query = {"domain_name": "python.test.domain5", "kind": "web"} + req = self.client.keys.get(filters=query) + self.assertEqual(req.status_code, 200) + self.assertIn("total_count", req.json()) + self.assertIn("items", req.json()) + + def test_get_keys_with_invalid_url(self) -> None: + query = {"domain_name": "python.test.domain5", "kind": "web"} + with self.assertRaises(KeyError): + self.client.key.get(filters=query) + + @patch("mailgun.client.requests.get") + def test_get_keys_without_filtering_data(self, m_get: MagicMock) -> None: + m_get.return_value = mock_response( + 200, {"items": [{"id": "k1", "description": "test"}]} + ) + req = self.client.keys.get() + self.assertEqual(req.status_code, 200) + self.assertGreater(len(req.json()["items"]), 0) + + @patch("mailgun.client.requests.post") + def test_post_keys(self, m_post: MagicMock) -> None: + m_post.return_value = mock_response( + 200, + { + "message": "great success", + "key": { + "id": "kid", + "description": "a new key", + "kind": "web", + "role": self.role, + "created_at": "", + "updated_at": "", + "expires_at": "", + "secret": "secret", + "is_disabled": False, + "domain_name": "python.test.domain5", + "requestor": self.mailgun_email, + "user_name": self.user_name, + }, + }, + ) + data = { + "email": self.mailgun_email, + "domain_name": "python.test.domain5", + "kind": "web", + "expiration": "3600", + "role": self.role, + "user_id": self.user_id, + "user_name": self.user_name, + "description": "a new key", + } + headers = {"Content-Type": "multipart/form-data"} + req = self.client.keys.create(data=data, headers=headers) + self.assertEqual(req.status_code, 200) + self.assertEqual(req.json()["message"], "great success") + self.assertIn("key", req.json()) + + @patch("mailgun.client.requests.delete") + @patch("mailgun.client.requests.get") + def test_delete_key(self, m_get: MagicMock, m_delete: MagicMock) -> None: + m_get.return_value = mock_response( + 200, + {"items": [{"id": "key1", "requestor": self.mailgun_email}]}, + ) + m_delete.return_value = mock_response(200, {"message": "key deleted"}) + req1 = self.client.keys.get(filters={"domain_name": "python.test.domain5", "kind": "web"}) + for item in req1.json()["items"]: + if self.mailgun_email == item["requestor"]: + req2 = self.client.keys.delete(key_id=item["id"]) + self.assertEqual(req2.json()["message"], "key deleted") + break From 6be723d3109c05c8f97b12f8a9c36c049b73643e Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 18 Feb 2026 18:15:42 +0100 Subject: [PATCH 23/30] Updated Makefile --- Makefile | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 7e5a879..1927e17 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean clean-env clean-test clean-pyc clean-build clean-other help dev test test-debug test-cov pre-commit lint format format-docs analyze docs +.PHONY: all clean clean-env clean-test clean-pyc clean-build clean-other help dev test test-unit test-integration test-debug test-cov tests-cov-fail pre-commit lint format format-docs analyze docs .DEFAULT_GOAL := help # The `.ONESHELL` and setting `SHELL` allows us to run commands that require @@ -89,7 +89,7 @@ environment: ## handles environment creation conda run --name $(CONDA_ENV_NAME) pip install . environment-dev: ## Handles environment creation - conda env create -n $(CONDA_ENV_NAME)-dev -y --file environment-dev.yml + conda env create -n $(CONDA_ENV_NAME)-dev -y --file environment-dev.yaml conda run --name $(CONDA_ENV_NAME)-dev pip install -e . install: clean ## install the package to the active Python's site-packages @@ -128,21 +128,27 @@ check-env: exit 1; \ fi -test: check-env ## runs test cases - $(PYTHON3) -m pytest -v --capture=no $(TEST_DIR)/tests.py +test: test-unit -test-debug: check-env ## runs test cases with debugging info enabled - $(PYTHON3) -m pytest -vv --capture=no $(TEST_DIR)/tests.py +test-unit: ## run unit tests only (no API key required) + $(PYTHON3) -m pytest -v --capture=no $(TEST_DIR)/unit/ -test-cov: check-env ## checks test coverage requirements +test-integration: check-env ## run integration tests only (requires APIKEY and DOMAIN) + $(PYTHON3) -m pytest -v --capture=no $(TEST_DIR)/integration/ + +test-debug: ## run unit tests with debugging info + $(PYTHON3) -m pytest -vv --capture=no $(TEST_DIR)/unit/ + +test-cov: ## check test coverage (unit tests only) $(PYTHON3) -m pytest --cov-config=.coveragerc --cov=$(SRC_DIR) \ - $(TEST_DIR)/tests.py --cov-fail-under=80 --cov-report term-missing + $(TEST_DIR)/unit/ --cov-report term-missing -tests-cov-fail: - @pytest --cov=$(SRC_DIR) --cov-report term-missing --cov-report=html --cov-fail-under=80 +test-cov-fail: ## check test coverage with fail-under (unit tests only) + $(PYTHON3) -m pytest --cov-config=.coveragerc --cov=$(SRC_DIR) \ + $(TEST_DIR)/unit/ --cov-fail-under=80 --cov-report term-missing --cov-report=html -coverage: ## check code coverage quickly with the default Python - coverage run --source $(SRC_DIR) -m pytest +coverage: ## check code coverage quickly with the default Python (unit tests only) + coverage run --source $(SRC_DIR) -m pytest $(TEST_DIR)/unit/ coverage report -m coverage html $(BROWSER) htmlcov/index.html From dd8b8d9c3b480c577985914e6e8689a3f445b46a Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Mon, 23 Feb 2026 17:09:55 +0100 Subject: [PATCH 24/30] Added unit tests for handlers --- tests/unit/test_handlers.py | 304 ++++++++++++++++++++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 tests/unit/test_handlers.py diff --git a/tests/unit/test_handlers.py b/tests/unit/test_handlers.py new file mode 100644 index 0000000..fb4333b --- /dev/null +++ b/tests/unit/test_handlers.py @@ -0,0 +1,304 @@ +"""Unit tests for mailgun handlers.""" + +import pytest + +from mailgun.handlers.default_handler import handle_default +from mailgun.handlers.domains_handler import ( + handle_dkimkeys, + handle_domainlist, + handle_domains, + handle_mailboxes_credentials, + handle_sending_queues, +) +from mailgun.handlers.email_validation_handler import handle_address_validate +from mailgun.handlers.error_handler import ApiError +from mailgun.handlers.inbox_placement_handler import handle_inbox +from mailgun.handlers.ips_handler import handle_ips +from mailgun.handlers.messages_handler import handle_resend_message +from mailgun.handlers.suppressions_handler import ( + handle_bounces, + handle_complaints, + handle_unsubscribes, + handle_whitelists, +) +from mailgun.handlers.tags_handler import handle_tags + + +class TestHandleDefault: + """Tests for handle_default.""" + + def test_requires_domain(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["messages"]} + with pytest.raises(ApiError, match="Domain is missing"): + handle_default(url, None, "get") + + def test_builds_url_with_domain(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["messages"]} + result = handle_default(url, "example.com", "get") + assert result == "https://api.mailgun.net/v3/example.com/messages" + + def test_builds_url_with_keys(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["events"]} + result = handle_default(url, "example.com", "get") + assert "example.com" in result + assert "events" in result + + +class TestHandleDomainlist: + """Tests for handle_domainlist.""" + + def test_returns_base_plus_domains(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + result = handle_domainlist(url, None, None) + assert result == "https://api.mailgun.net/v4/domains" + + +class TestHandleDomains: + """Tests for handle_domains.""" + + def test_with_domain_and_keys(self) -> None: + url = {"base": "https://api.mailgun.net/v4/domains/", "keys": ["webhooks"]} + result = handle_domains(url, "example.com", "get") + assert "example.com" in result + assert "webhooks" in result + + def test_requires_domain_when_keys_present(self) -> None: + url = {"base": "https://api.mailgun.net/v4/domains/", "keys": ["webhooks"]} + with pytest.raises(ApiError, match="Domain is missing"): + handle_domains(url, None, "get") + + def test_with_login_kwarg(self) -> None: + url = {"base": "https://api.mailgun.net/v4/domains/", "keys": ["credentials"]} + result = handle_domains(url, "example.com", "get", login="user@example.com") + assert "user@example.com" in result or "login" in result + + def test_with_domain_name_kwarg_get(self) -> None: + url = {"base": "https://api.mailgun.net/v4/domains/", "keys": []} + result = handle_domains( + url, None, "get", domain_name="my-domain.com" + ) + assert "my-domain.com" in result + + def test_verify_requires_true(self) -> None: + url = {"base": "https://api.mailgun.net/v4/domains/", "keys": []} + with pytest.raises(ApiError, match="Verify option should be True"): + handle_domains(url, "example.com", "put", verify=False) + + +class TestHandleSendingQueues: + """Tests for handle_sending_queues.""" + + def test_builds_sending_queues_url(self) -> None: + url = {"base": "https://api.mailgun.net/v4/domains/", "keys": ["sending_queues"]} + result = handle_sending_queues(url, "example.com", None) + assert result.endswith("/example.com/sending_queues") + assert "sending_queues" in result + + +class TestHandleMailboxesCredentials: + """Tests for handle_mailboxes_credentials.""" + + def test_with_login(self) -> None: + url = {"base": "https://api.mailgun.net/v3/domains/", "keys": ["credentials"]} + result = handle_mailboxes_credentials( + url, "example.com", None, login="user@example.com" + ) + assert "user@example.com" in result + assert "credentials" in result + + +class TestHandleDkimkeys: + """Tests for handle_dkimkeys.""" + + def test_builds_dkim_keys_url(self) -> None: + url = {"base": "https://api.mailgun.net/v1/", "keys": ["dkim", "keys"]} + result = handle_dkimkeys(url, None, None) + assert "dkim" in result + assert "keys" in result + + +class TestHandleIps: + """Tests for handle_ips.""" + + def test_base_without_trailing_slash(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["ips"]} + result = handle_ips(url, None, None) + assert result == "https://api.mailgun.net/v3/ips" + + def test_with_ip_kwarg(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["ips"]} + result = handle_ips(url, None, None, ip="1.2.3.4") + assert "1.2.3.4" in result + + +class TestHandleTags: + """Tests for handle_tags.""" + + def test_builds_tags_url_with_domain(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["tags"]} + result = handle_tags(url, "example.com", None) + assert "example.com" in result + assert "tags" in result + + def test_with_tag_name_kwarg(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["tags"]} + result = handle_tags(url, "example.com", None, tag_name="my-tag") + assert "my-tag" in result + + +class TestHandleBounces: + """Tests for handle_bounces.""" + + def test_with_domain(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["bounces"]} + result = handle_bounces(url, "example.com", None) + assert "example.com" in result + assert "bounces" in result + + def test_with_bounce_address(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["bounces"]} + result = handle_bounces( + url, "example.com", None, bounce_address="bad@example.com" + ) + assert "bad@example.com" in result + + +class TestHandleUnsubscribes: + """Tests for handle_unsubscribes.""" + + def test_with_unsubscribe_address(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["unsubscribes"]} + result = handle_unsubscribes( + url, "example.com", None, unsubscribe_address="user@example.com" + ) + assert "user@example.com" in result + + +class TestHandleComplaints: + """Tests for handle_complaints.""" + + def test_with_complaint_address(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["complaints"]} + result = handle_complaints( + url, "example.com", None, complaint_address="spam@example.com" + ) + assert "spam@example.com" in result + + +class TestHandleWhitelists: + """Tests for handle_whitelists.""" + + def test_with_domain(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["whitelists"]} + result = handle_whitelists(url, "example.com", None) + assert "example.com" in result + assert "whitelists" in result + + +class TestHandleAddressValidate: + """Tests for handle_address_validate (email validation handler).""" + + def test_without_list_name_single_key(self) -> None: + """url["keys"][1:] is empty, no list_name.""" + url = {"base": "https://api.mailgun.net/v4/address/validate", "keys": ["validate"]} + result = handle_address_validate(url, None, None) + assert result == "https://api.mailgun.net/v4/address/validate" + + def test_without_list_name_multiple_keys(self) -> None: + """url["keys"][1:] is non-empty, no list_name.""" + url = { + "base": "https://api.mailgun.net/v4/address/validate", + "keys": ["validate", "bulk"], + } + result = handle_address_validate(url, None, None) + assert result == "https://api.mailgun.net/v4/address/validate/bulk" + + def test_with_list_name(self) -> None: + """list_name in kwargs appends /list_name to path.""" + url = { + "base": "https://api.mailgun.net/v4/address/validate", + "keys": ["validate", "bulk"], + } + result = handle_address_validate( + url, None, None, list_name="my_list" + ) + assert result == "https://api.mailgun.net/v4/address/validate/bulk/my_list" + + def test_with_list_name_single_key(self) -> None: + """list_name with single key (final_keys empty).""" + url = {"base": "https://api.mailgun.net/v4/address/validate", "keys": ["validate"]} + result = handle_address_validate(url, None, None, list_name="my_list") + assert result == "https://api.mailgun.net/v4/address/validate/my_list" + + +class TestHandleInbox: + """Tests for handle_inbox (inbox placement handler).""" + + def test_no_test_id_empty_keys(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": []} + result = handle_inbox(url, None, None) + assert result == "https://api.mailgun.net/v3" + + def test_no_test_id_with_keys(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["inbox", "tests"]} + result = handle_inbox(url, None, None) + assert result == "https://api.mailgun.net/v3/inbox/tests" + + def test_with_test_id_only(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["inbox", "tests"]} + result = handle_inbox(url, None, None, test_id="test-123") + assert result == "https://api.mailgun.net/v3/inbox/tests/test-123" + + def test_with_test_id_and_counters_true(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["inbox", "tests"]} + result = handle_inbox( + url, None, None, test_id="test-123", counters=True + ) + assert result == "https://api.mailgun.net/v3/inbox/tests/test-123/counters" + + def test_with_test_id_and_counters_false_raises(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["inbox", "tests"]} + with pytest.raises(ApiError, match="Counters option should be True or absent"): + handle_inbox(url, None, None, test_id="test-123", counters=False) + + def test_with_test_id_and_checks_true_no_address(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["inbox", "tests"]} + result = handle_inbox( + url, None, None, test_id="test-123", checks=True + ) + assert result == "https://api.mailgun.net/v3/inbox/tests/test-123/checks" + + def test_with_test_id_and_checks_true_with_address(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["inbox", "tests"]} + result = handle_inbox( + url, + None, + None, + test_id="test-123", + checks=True, + address="user@example.com", + ) + assert result == ( + "https://api.mailgun.net/v3/inbox/tests/test-123/checks/user@example.com" + ) + + def test_with_test_id_and_checks_false_raises(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["inbox", "tests"]} + with pytest.raises(ApiError, match="Checks option should be True or absent"): + handle_inbox(url, None, None, test_id="test-123", checks=False) + + +class TestHandleResendMessage: + """Tests for handle_resend_message.""" + + def test_with_storage_url(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["resendmessage"]} + result = handle_resend_message( + url, None, None, storage_url="https://storage.mailgun.net/msg/123" + ) + assert result == "https://storage.mailgun.net/msg/123" + + def test_without_storage_url_returns_none(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["resendmessage"]} + result = handle_resend_message(url, None, None) + assert result is None From 9b3cc6a2b61e548694712a67f62f8f97a09e6bd1 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Tue, 24 Feb 2026 18:33:17 +0100 Subject: [PATCH 25/30] Added unit tests for clients --- tests/unit/test_async_client.py | 144 +++++++++++++++++++++++++++++ tests/unit/test_client.py | 157 ++++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 tests/unit/test_async_client.py create mode 100644 tests/unit/test_client.py diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py new file mode 100644 index 0000000..4c30c9f --- /dev/null +++ b/tests/unit/test_async_client.py @@ -0,0 +1,144 @@ +"""Unit tests for mailgun.client (AsyncClient, AsyncEndpoint).""" + +import io +from unittest.mock import AsyncMock +from unittest.mock import MagicMock + +import httpx +import pytest + +from mailgun.client import AsyncClient +from mailgun.client import AsyncEndpoint +from mailgun.client import Config +from mailgun.handlers.error_handler import ApiError + + +class TestAsyncEndpointPrepareFiles: + """Tests for AsyncEndpoint._prepare_files.""" + + def _make_endpoint(self) -> AsyncEndpoint: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["messages"]} + return AsyncEndpoint( + url=url, + headers={}, + auth=None, + client=MagicMock(spec=httpx.AsyncClient), + ) + + def test_prepare_files_none(self) -> None: + ep = self._make_endpoint() + assert ep._prepare_files(None) is None + + def test_prepare_files_dict_bytes(self) -> None: + ep = self._make_endpoint() + files = {"attachment": b"binary content"} + result = ep._prepare_files(files) + assert result is not None + assert "attachment" in result + # (filename, file_obj, content_type) + assert len(result["attachment"]) == 3 + assert result["attachment"][0] == "attachment" + assert isinstance(result["attachment"][1], io.BytesIO) + assert result["attachment"][1].read() == b"binary content" + assert result["attachment"][2] == "application/octet-stream" + + def test_prepare_files_dict_tuple(self) -> None: + ep = self._make_endpoint() + files = {"f": ("name.txt", b"data", "text/plain")} + result = ep._prepare_files(files) + assert result is not None + assert result["f"][0] == "name.txt" + assert result["f"][2] == "text/plain" + + +class TestAsyncEndpoint: + """Tests for AsyncEndpoint with mocked httpx.""" + + @pytest.mark.asyncio + async def test_get_calls_client_request(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.request = AsyncMock( + return_value=MagicMock(status_code=200, spec=httpx.Response) + ) + ep = AsyncEndpoint(url=url, headers={"User-agent": "test"}, auth=("api", "key"), client=mock_client) + await ep.get() + mock_client.request.assert_called_once() + assert mock_client.request.call_args[1]["method"] == "GET" + + @pytest.mark.asyncio + async def test_create_sends_post(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.request = AsyncMock( + return_value=MagicMock(status_code=200, spec=httpx.Response) + ) + ep = AsyncEndpoint(url=url, headers={}, auth=None, client=mock_client) + await ep.create(data={"name": "test.com"}) + mock_client.request.assert_called_once() + assert mock_client.request.call_args[1]["method"] == "POST" + + @pytest.mark.asyncio + async def test_delete_calls_client_request(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.request = AsyncMock( + return_value=MagicMock(status_code=200, spec=httpx.Response) + ) + ep = AsyncEndpoint(url=url, headers={}, auth=None, client=mock_client) + await ep.delete() + assert mock_client.request.call_args[1]["method"] == "DELETE" + + @pytest.mark.asyncio + async def test_api_call_raises_timeout_error(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.request = AsyncMock(side_effect=httpx.TimeoutException("timeout")) + ep = AsyncEndpoint(url=url, headers={}, auth=None, client=mock_client) + with pytest.raises(TimeoutError): + await ep.get() + + @pytest.mark.asyncio + async def test_api_call_raises_api_error_on_request_error(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + mock_client = AsyncMock(spec=httpx.AsyncClient) + mock_client.request = AsyncMock(side_effect=httpx.RequestError("error")) + ep = AsyncEndpoint(url=url, headers={}, auth=None, client=mock_client) + with pytest.raises(ApiError): + await ep.get() + + +class TestAsyncClient: + """Tests for AsyncClient.""" + + def test_async_client_inherits_client(self) -> None: + client = AsyncClient(auth=("api", "key")) + assert isinstance(client, AsyncClient) + assert client.auth == ("api", "key") + assert client.config.api_url == Config.DEFAULT_API_URL + + def test_async_client_getattr_returns_async_endpoint_type(self) -> None: + client = AsyncClient(auth=("api", "key")) + ep = client.domains + assert ep is not None + assert isinstance(ep, AsyncEndpoint) + assert type(ep).__name__ == "domains" + + @pytest.mark.asyncio + async def test_aclose_closes_httpx_client(self) -> None: + client = AsyncClient(auth=("api", "key")) + # Trigger _client creation + _ = client.domains + assert client._httpx_client is None or not client._httpx_client.is_closed + # Access property to create client + _ = client._client + await client.aclose() + assert client._httpx_client.is_closed + + @pytest.mark.asyncio + async def test_async_context_manager(self) -> None: + async with AsyncClient(auth=("api", "key")) as client: + assert client is not None + assert isinstance(client, AsyncClient) + # After exit, client should be closed + assert client._httpx_client is None or client._httpx_client.is_closed diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py new file mode 100644 index 0000000..5759b34 --- /dev/null +++ b/tests/unit/test_client.py @@ -0,0 +1,157 @@ +"""Unit tests for mailgun.client (Client, Config, Endpoint).""" + +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +import requests + +from mailgun.client import BaseEndpoint +from mailgun.client import Client +from mailgun.client import Config +from mailgun.client import Endpoint +from mailgun.handlers.error_handler import ApiError + + +class TestClient: + """Tests for Client class.""" + + def test_client_init_default(self) -> None: + client = Client() + assert client.auth is None + assert client.config.api_url == Config.DEFAULT_API_URL + + def test_client_init_with_auth(self) -> None: + client = Client(auth=("api", "key-123")) + assert client.auth == ("api", "key-123") + + def test_client_init_with_api_url(self) -> None: + client = Client(api_url="https://custom.api/") + assert client.config.api_url == "https://custom.api/" + + def test_client_getattr_returns_endpoint_type(self) -> None: + client = Client(auth=("api", "key-123")) + ep = client.domains + assert ep is not None + assert isinstance(ep, Endpoint) + assert type(ep).__name__ == "domains" + + def test_client_getattr_ips(self) -> None: + client = Client(auth=("api", "key-123")) + ep = client.ips + assert type(ep).__name__ == "ips" + + +class TestBaseEndpointBuildUrl: + """Tests for BaseEndpoint.build_url (static, dispatches to handlers).""" + + def test_build_url_domains_with_domain(self) -> None: + # With domain_name in kwargs, handle_domains includes it in the URL + url = {"base": "https://api.mailgun.net/v4/domains/", "keys": ["domains"]} + result = BaseEndpoint.build_url( + url, domain="example.com", method="get", domain_name="example.com" + ) + assert "example.com" in result + + def test_build_url_domainlist(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + result = BaseEndpoint.build_url(url, domain=None, method="get") + assert "domains" in result + + def test_build_url_default_requires_domain(self) -> None: + url = {"base": "https://api.mailgun.net/v3/", "keys": ["messages"]} + with pytest.raises(ApiError, match="Domain is missing"): + BaseEndpoint.build_url(url, domain=None, method="post") + + +class TestEndpoint: + """Tests for Endpoint (sync) with mocked HTTP.""" + + def test_get_calls_requests_get(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + headers = {"User-agent": "test"} + auth = ("api", "key-123") + ep = Endpoint(url=url, headers=headers, auth=auth) + with patch.object(requests, "get", return_value=MagicMock(status_code=200)) as m_get: + resp = ep.get() + m_get.assert_called_once() + call_kw = m_get.call_args[1] + assert call_kw["auth"] == auth + assert call_kw["headers"] == headers + assert "domainlist" in m_get.call_args[0][0] or "domains" in m_get.call_args[0][0] + + def test_get_with_filters(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint(url=url, headers={}, auth=None) + with patch.object(requests, "get", return_value=MagicMock(status_code=200)) as m_get: + ep.get(filters={"limit": 10}) + m_get.assert_called_once() + assert m_get.call_args[1]["params"] == {"limit": 10} + + def test_create_sends_post(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint(url=url, headers={}, auth=("api", "key")) + with patch.object(requests, "post", return_value=MagicMock(status_code=200)) as m_post: + ep.create(data={"name": "test.com"}) + m_post.assert_called_once() + assert m_post.call_args[1]["data"] is not None + + def test_create_json_serializes_when_content_type_json(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint( + url=url, + headers={"Content-Type": "application/json"}, + auth=None, + ) + with patch.object(requests, "post", return_value=MagicMock(status_code=200)) as m_post: + ep.create(data={"name": "test.com"}) + call_data = m_post.call_args[1]["data"] + assert call_data == '{"name": "test.com"}' + + def test_delete_calls_requests_delete(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint(url=url, headers={}, auth=None) + with patch.object(requests, "delete", return_value=MagicMock(status_code=200)) as m_del: + ep.delete() + m_del.assert_called_once() + + def test_put_calls_requests_put(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint(url=url, headers={}, auth=None) + with patch.object(requests, "put", return_value=MagicMock(status_code=200)) as m_put: + ep.put(data={"key": "value"}) + m_put.assert_called_once() + + def test_patch_calls_requests_patch(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint(url=url, headers={}, auth=None) + with patch.object(requests, "patch", return_value=MagicMock(status_code=200)) as m_patch: + ep.patch(data={"key": "value"}) + m_patch.assert_called_once() + + def test_api_call_raises_timeout_error_on_timeout(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint(url=url, headers={}, auth=None) + with patch.object(requests, "get", side_effect=requests.exceptions.Timeout()): + with pytest.raises(TimeoutError): + ep.get() + + def test_api_call_raises_api_error_on_request_exception(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint(url=url, headers={}, auth=None) + with patch.object( + requests, "get", side_effect=requests.exceptions.RequestException("network error") + ): + with pytest.raises(ApiError): + ep.get() + + def test_update_serializes_json(self) -> None: + url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} + ep = Endpoint( + url=url, + headers={"Content-type": "application/json"}, + auth=None, + ) + with patch.object(requests, "put", return_value=MagicMock(status_code=200)) as m_put: + ep.update(data={"name": "updated.com"}) + assert m_put.call_args[1]["data"] == '{"name": "updated.com"}' From b00982ed4ea49f9fc93d22e4a29e0ea59fc00240 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Thu, 26 Feb 2026 18:19:45 +0100 Subject: [PATCH 26/30] Added unit tests to improve coverage --- tests/unit/test_error_handler.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/unit/test_error_handler.py diff --git a/tests/unit/test_error_handler.py b/tests/unit/test_error_handler.py new file mode 100644 index 0000000..5685724 --- /dev/null +++ b/tests/unit/test_error_handler.py @@ -0,0 +1,31 @@ +"""Unit tests for mailgun.handlers.error_handler.""" + +import pytest + +from mailgun.handlers.error_handler import ApiError + + +class TestApiError: + """Tests for ApiError exception.""" + + def test_api_error_is_exception(self) -> None: + assert issubclass(ApiError, Exception) + + def test_api_error_message(self) -> None: + err = ApiError("Domain is missing!") + assert str(err) == "Domain is missing!" + + def test_api_error_can_be_raised(self) -> None: + with pytest.raises(ApiError) as exc_info: + raise ApiError("Storage url is required") + assert exc_info.value.args[0] == "Storage url is required" + + def test_api_error_with_cause(self) -> None: + try: + raise ValueError("inner") + except ValueError as e: + try: + raise ApiError("wrapped") from e + except ApiError as err: + assert str(err) == "wrapped" + assert err.__cause__ is not None From a09bf176c38c0c44b79c294147a6fad3eaee392e Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Thu, 26 Feb 2026 18:55:09 +0100 Subject: [PATCH 27/30] Added unit tests to improve coverage --- tests/unit/test_config.py | 109 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/unit/test_config.py diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py new file mode 100644 index 0000000..9d76626 --- /dev/null +++ b/tests/unit/test_config.py @@ -0,0 +1,109 @@ +"""Unit tests for mailgun.client.Config.""" + +import pytest + +from mailgun.client import Config + + +class TestConfig: + """Tests for Config class.""" + + def test_default_api_url(self) -> None: + config = Config() + assert config.api_url == Config.DEFAULT_API_URL + assert config.api_url == "https://api.mailgun.net/" + + def test_custom_api_url(self) -> None: + config = Config(api_url="https://custom.api/") + assert config.api_url == "https://custom.api/" + + def test_getitem_messages(self) -> None: + config = Config() + url, headers = config["messages"] + assert "base" in url + assert url["keys"] == ["messages"] + assert "User-agent" in headers + + def test_getitem_domains(self) -> None: + config = Config() + url, headers = config["domains"] + assert "base" in url + assert "domains" in str(url["keys"]).lower() or "domains" in url["keys"] + assert "User-agent" in headers + + def test_getitem_domainlist(self) -> None: + config = Config() + url, headers = config["domainlist"] + assert "base" in url + assert url["keys"] == ["domainlist"] + assert config["domainlist"][0]["base"].endswith("v4/") + + def test_getitem_ips(self) -> None: + config = Config() + url, headers = config["ips"] + assert url["keys"] == ["ips"] + assert "v3" in url["base"] + + def test_getitem_tags(self) -> None: + config = Config() + url, headers = config["tags"] + assert url["keys"] == ["tags"] + + def test_getitem_bounces(self) -> None: + config = Config() + url, headers = config["bounces"] + assert url["keys"] == ["bounces"] + + def test_getitem_dkim(self) -> None: + config = Config() + url, headers = config["dkim"] + assert url["keys"] == ["dkim", "keys"] + assert "v1" in url["base"] + + def test_getitem_analytics(self) -> None: + config = Config() + url, headers = config["analytics"] + # "analytics" is in special_cases, so returns early without Content-Type + assert "analytics" in url["keys"] + assert url["keys"] == ["analytics", "usage", "metrics", "logs", "tags", "limits"] + + def test_getitem_analytics_metrics_has_content_type(self) -> None: + """Keys containing 'analytics' (but not exact 'analytics') get Content-Type.""" + config = Config() + url, headers = config["analytics_metrics"] + assert "analytics" in url["keys"] + assert headers.get("Content-Type") == "application/json" + + def test_getitem_users(self) -> None: + config = Config() + url, headers = config["users"] + assert "users" in url["keys"] + assert "v5" in url["base"] + + def test_getitem_keys(self) -> None: + config = Config() + url, headers = config["keys"] + assert "keys" in url["keys"] + assert "v1" in url["base"] + + def test_getitem_case_insensitive(self) -> None: + config = Config() + url1, _ = config["DOMAINS"] + url2, _ = config["domains"] + assert url1["keys"] == url2["keys"] + assert url1["base"] == url2["base"] + + def test_getitem_addressvalidate(self) -> None: + config = Config() + url, headers = config["addressvalidate"] + assert "address/validate" in url["base"] or "validate" in str(url["keys"]) + + def test_getitem_resendmessage(self) -> None: + config = Config() + url, _ = config["resendmessage"] + assert url["keys"] == ["resendmessage"] + + def test_getitem_ippools(self) -> None: + config = Config() + url, _ = config["ippools"] + assert url["keys"] == ["ip_pools"] From 321519c7dfafb6814e7f408a6a79959525a4e961 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Tue, 3 Mar 2026 13:22:08 +0100 Subject: [PATCH 28/30] Skip false positive in tests --- tests/unit/test_integration_mirror.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 98b7e1c..935199c 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -66,9 +66,9 @@ def setUp(self) -> None: self.put_domain_data = {"spam_action": "disabled"} self.post_domain_creds = { "login": f"alice_bob@{self.domain}", - "password": "test_new_creds123", + "password": "test_new_creds123", # pragma: allowlist secret } - self.put_domain_creds = {"password": "test_new_creds"} + self.put_domain_creds = {"password": "test_new_creds"} # pragma: allowlist secret self.put_domain_connections_data = {"require_tls": "false", "skip_verification": "false"} self.put_domain_tracking_data = {"active": "yes", "skip_verification": "false"} self.put_domain_unsubscribe_data = { @@ -1838,7 +1838,7 @@ def test_post_keys(self, m_post: MagicMock) -> None: "created_at": "", "updated_at": "", "expires_at": "", - "secret": "secret", + "secret": "secret", # pragma: allowlist secret "is_disabled": False, "domain_name": "python.test.domain5", "requestor": self.mailgun_email, From fe56a9abd152131b5c6bd93620a67bf027178660 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Tue, 3 Mar 2026 15:22:54 +0100 Subject: [PATCH 29/30] Linter fixes --- .pre-commit-config.yaml | 2 +- tests/unit/test_client.py | 6 +- tests/unit/test_config.py | 2 - tests/unit/test_integration_mirror.py | 88 +++++++++++++-------------- 4 files changed, 48 insertions(+), 50 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3df5f8..3096874 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -260,7 +260,7 @@ repos: hooks: - id: interrogate name: "๐Ÿ“ docs ยท Check docstring coverage" - args: [ --verbose, --fail-under=53, --ignore-init-method ] + args: [ --verbose, --fail-under=53, --ignore-init-method, --exclude "tests"] # Python type checking - repo: https://github.com/pre-commit/mirrors-mypy diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 5759b34..3401c38 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -55,13 +55,13 @@ def test_build_url_domains_with_domain(self) -> None: def test_build_url_domainlist(self) -> None: url = {"base": "https://api.mailgun.net/v4/", "keys": ["domainlist"]} - result = BaseEndpoint.build_url(url, domain=None, method="get") + result = BaseEndpoint.build_url(url, method="get") assert "domains" in result def test_build_url_default_requires_domain(self) -> None: url = {"base": "https://api.mailgun.net/v3/", "keys": ["messages"]} with pytest.raises(ApiError, match="Domain is missing"): - BaseEndpoint.build_url(url, domain=None, method="post") + BaseEndpoint.build_url(url, method="post") class TestEndpoint: @@ -73,7 +73,7 @@ def test_get_calls_requests_get(self) -> None: auth = ("api", "key-123") ep = Endpoint(url=url, headers=headers, auth=auth) with patch.object(requests, "get", return_value=MagicMock(status_code=200)) as m_get: - resp = ep.get() + ep.get() m_get.assert_called_once() call_kw = m_get.call_args[1] assert call_kw["auth"] == auth diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 9d76626..f3fa759 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,7 +1,5 @@ """Unit tests for mailgun.client.Config.""" -import pytest - from mailgun.client import Config diff --git a/tests/unit/test_integration_mirror.py b/tests/unit/test_integration_mirror.py index 935199c..8f85b2b 100644 --- a/tests/unit/test_integration_mirror.py +++ b/tests/unit/test_integration_mirror.py @@ -106,7 +106,7 @@ def test_post_domain_creds(self, m_post: MagicMock) -> None: def test_update_simple_domain( self, m_put: MagicMock, m_post: MagicMock, m_delete: MagicMock ) -> None: - m_delete.return_value = mock_response(200) + m_delete.return_value = mock_response() m_post.return_value = mock_response(200, {"domain": {}}) m_put.return_value = mock_response(200, {"message": "Domain has been updated"}) self.client.domains.create(data=self.post_domain_data) @@ -133,7 +133,7 @@ def test_put_domain_creds(self, m_put: MagicMock, m_post: MagicMock) -> None: @patch("mailgun.client.requests.post") @patch("mailgun.client.requests.put") def test_put_mailboxes_credentials(self, m_put: MagicMock, m_post: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response( 200, { @@ -175,7 +175,7 @@ def test_get_sending_queues(self, m_get: MagicMock) -> None: @patch("mailgun.client.requests.get") @patch("mailgun.client.requests.post") def test_get_single_domain(self, m_post: MagicMock, m_get: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_get.return_value = mock_response(200, {"domain": {"name": self.test_domain}}) self.client.domains.create(data=self.post_domain_data) req = self.client.domains.get(domain_name=self.post_domain_data["name"]) @@ -185,7 +185,7 @@ def test_get_single_domain(self, m_post: MagicMock, m_get: MagicMock) -> None: @patch("mailgun.client.requests.put") @patch("mailgun.client.requests.post") def test_verify_domain(self, m_post: MagicMock, m_put: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response(200, {"domain": {"state": "verified"}}) self.client.domains.create(data=self.post_domain_data) req = self.client.domains.put(domain=self.post_domain_data["name"], verify=True) @@ -231,7 +231,7 @@ def test_put_domain_unsubscribe(self, m_put: MagicMock) -> None: @patch("mailgun.client.requests.put") @patch("mailgun.client.requests.post") def test_put_dkim_authority(self, m_post: MagicMock, m_put: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response(200, {"message": "Updated"}) self.client.domains.create(data=self.post_domain_data) request = self.client.domains_dkimauthority.put( @@ -242,7 +242,7 @@ def test_put_dkim_authority(self, m_post: MagicMock, m_put: MagicMock) -> None: @patch("mailgun.client.requests.put") @patch("mailgun.client.requests.post") def test_put_webprefix(self, m_post: MagicMock, m_put: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response(200, {"message": "Updated"}) self.client.domains.create(data=self.post_domain_data) request = self.client.domains_webprefix.put( @@ -295,8 +295,8 @@ def test_delete_dkim_keys(self, m_delete: MagicMock) -> None: @patch("mailgun.client.requests.delete") @patch("mailgun.client.requests.post") def test_delete_domain_creds(self, m_post: MagicMock, m_delete: MagicMock) -> None: - m_post.return_value = mock_response(200) - m_delete.return_value = mock_response(200) + m_post.return_value = mock_response() + m_delete.return_value = mock_response() self.client.domains_credentials.create( domain=self.domain, data=self.post_domain_creds ) @@ -310,7 +310,7 @@ def test_delete_domain_creds(self, m_post: MagicMock, m_delete: MagicMock) -> No def test_delete_all_domain_credentials( self, m_post: MagicMock, m_delete: MagicMock ) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_delete.return_value = mock_response( 200, {"message": "All domain credentials have been deleted"} ) @@ -326,7 +326,7 @@ def test_delete_all_domain_credentials( @patch("mailgun.client.requests.delete") @patch("mailgun.client.requests.post") def test_delete_domain(self, m_post: MagicMock, m_delete: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_delete.return_value = mock_response( 200, {"message": "Domain will be deleted in the background"} ) @@ -356,7 +356,7 @@ def test_get_ip_from_domain(self, m_get: MagicMock) -> None: @patch("mailgun.client.requests.get") @patch("mailgun.client.requests.post") def test_get_ip_by_address(self, m_post: MagicMock, m_get: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_get.return_value = mock_response(200, {"ip": self.ip_data["ip"]}) self.client.domains_ips.create(domain=self.domain, data=self.ip_data) req = self.client.ips.get(domain=self.domain, ip=self.ip_data["ip"]) @@ -560,7 +560,7 @@ def test_bounces_create(self, m_post: MagicMock) -> None: @patch("mailgun.client.requests.get") @patch("mailgun.client.requests.post") def test_bounces_get_address(self, m_post: MagicMock, m_get: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_get.return_value = mock_response(200, {"address": self.bounces_data["address"]}) self.client.bounces.create(data=self.bounces_data, domain=self.domain) req = self.client.bounces.get( @@ -585,7 +585,7 @@ def test_bounces_create_json(self, m_post: MagicMock) -> None: @patch("mailgun.client.requests.delete") @patch("mailgun.client.requests.post") def test_bounces_delete_single(self, m_post: MagicMock, m_delete: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_delete.return_value = mock_response(200, {"message": "Deleted"}) self.client.bounces.create(data=self.bounces_data, domain=self.domain) req = self.client.bounces.delete( @@ -699,7 +699,7 @@ def test_compl_get_all(self, m_get: MagicMock) -> None: @patch("mailgun.client.requests.get") @patch("mailgun.client.requests.post") def test_compl_get_single(self, m_post: MagicMock, m_get: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_get.return_value = mock_response(200, {"address": self.compl_data["address"]}) self.client.complaints.create(data=self.compl_data, domain=self.domain) req = self.client.complaints.get( @@ -724,7 +724,7 @@ def test_compl_create_multiple(self, m_post: MagicMock) -> None: @patch("mailgun.client.requests.delete") @patch("mailgun.client.requests.post") def test_compl_delete_single(self, m_post: MagicMock, m_delete: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_delete.return_value = mock_response(200, {"message": "Deleted"}) req = self.client.complaints.delete( domain=self.domain, unsubscribe_address=self.compl_data["address"] @@ -795,7 +795,7 @@ def test_routes_create( self, m_get: MagicMock, m_delete: MagicMock, m_post: MagicMock ) -> None: m_get.return_value = mock_response(200, {"items": [{"id": "rid"}]}) - m_delete.return_value = mock_response(200) + m_delete.return_value = mock_response() m_post.return_value = mock_response(200, {"message": "Route created"}) params = {"skip": 0, "limit": 1} req1 = self.client.routes.get(domain=self.domain, filters=params) @@ -897,7 +897,7 @@ def setUp(self) -> None: @patch("mailgun.client.requests.post") def test_webhooks_create(self, m_post: MagicMock, m_delete: MagicMock) -> None: m_post.return_value = mock_response(200, {"message": "Created"}) - m_delete.return_value = mock_response(200) + m_delete.return_value = mock_response() req = self.client.domains_webhooks.create( domain=self.domain, data=self.webhooks_data ) @@ -915,7 +915,7 @@ def test_webhooks_get(self, m_get: MagicMock) -> None: @patch("mailgun.client.requests.put") @patch("mailgun.client.requests.post") def test_webhook_put(self, m_post: MagicMock, m_put: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response(200, {"message": "Updated"}) self.client.domains_webhooks.create( domain=self.domain, data=self.webhooks_data @@ -930,7 +930,7 @@ def test_webhook_put(self, m_post: MagicMock, m_put: MagicMock) -> None: @patch("mailgun.client.requests.get") @patch("mailgun.client.requests.post") def test_webhook_get_simple(self, m_post: MagicMock, m_get: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_get.return_value = mock_response(200, {"webhook": {}}) self.client.domains_webhooks.create( domain=self.domain, data=self.webhooks_data @@ -943,7 +943,7 @@ def test_webhook_get_simple(self, m_post: MagicMock, m_get: MagicMock) -> None: @patch("mailgun.client.requests.delete") @patch("mailgun.client.requests.post") def test_webhook_delete(self, m_post: MagicMock, m_delete: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_delete.return_value = mock_response(200, {"message": "Deleted"}) self.client.domains_webhooks.create( domain=self.domain, data=self.webhooks_data @@ -1012,7 +1012,7 @@ def test_maillist_lists_create(self, m_post: MagicMock) -> None: @patch("mailgun.client.requests.put") @patch("mailgun.client.requests.post") def test_maillists_lists_put(self, m_post: MagicMock, m_put: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response(200, {"list": {}}) self.client.lists.create(domain=self.domain, data=self.mailing_lists_data) req = self.client.lists.put( @@ -1028,8 +1028,8 @@ def test_maillists_lists_put(self, m_post: MagicMock, m_put: MagicMock) -> None: def test_maillists_lists_delete( self, m_post: MagicMock, m_delete: MagicMock ) -> None: - m_post.return_value = mock_response(200) - m_delete.return_value = mock_response(200) + m_post.return_value = mock_response() + m_delete.return_value = mock_response() self.client.lists.create(domain=self.domain, data=self.mailing_lists_data) req = self.client.lists.delete( domain=self.domain, address=f"python_sdk@{self.domain}" @@ -1051,7 +1051,7 @@ def test_maillists_lists_members_pages_get(self, m_get: MagicMock) -> None: def test_maillists_lists_members_create( self, m_delete: MagicMock, m_post: MagicMock ) -> None: - m_delete.return_value = mock_response(200) + m_delete.return_value = mock_response() m_post.return_value = mock_response(200, {"member": {}}) req = self.client.lists_members.create( domain=self.domain, @@ -1075,7 +1075,7 @@ def test_maillists_lists_members_get(self, m_get: MagicMock) -> None: def test_maillists_lists_members_update( self, m_post: MagicMock, m_put: MagicMock ) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response(200, {"member": {}}) self.client.lists_members.create( domain=self.domain, @@ -1096,8 +1096,8 @@ def test_maillists_lists_members_update( def test_maillists_lists_members_delete( self, m_post: MagicMock, m_delete: MagicMock ) -> None: - m_post.return_value = mock_response(200) - m_delete.return_value = mock_response(200) + m_post.return_value = mock_response() + m_delete.return_value = mock_response() self.client.lists_members.create( domain=self.domain, address=self.maillist_address, @@ -1153,7 +1153,7 @@ def setUp(self) -> None: @patch("mailgun.client.requests.delete") @patch("mailgun.client.requests.post") def test_create_template(self, m_post: MagicMock, m_delete: MagicMock) -> None: - m_delete.return_value = mock_response(200) + m_delete.return_value = mock_response() m_post.return_value = mock_response(200, {"template": {}}) self.client.templates.delete( domain=self.domain, @@ -1168,7 +1168,7 @@ def test_create_template(self, m_post: MagicMock, m_delete: MagicMock) -> None: @patch("mailgun.client.requests.get") @patch("mailgun.client.requests.post") def test_get_template(self, m_post: MagicMock, m_get: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_get.return_value = mock_response(200, {"template": {}}) params = {"active": "yes"} req = self.client.templates.get( @@ -1182,7 +1182,7 @@ def test_get_template(self, m_post: MagicMock, m_get: MagicMock) -> None: @patch("mailgun.client.requests.put") @patch("mailgun.client.requests.post") def test_put_template(self, m_post: MagicMock, m_put: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response(200, {"template": {}}) self.client.templates.create( data=self.post_template_data, domain=self.domain @@ -1198,8 +1198,8 @@ def test_put_template(self, m_post: MagicMock, m_put: MagicMock) -> None: @patch("mailgun.client.requests.delete") @patch("mailgun.client.requests.post") def test_delete_template(self, m_post: MagicMock, m_delete: MagicMock) -> None: - m_post.return_value = mock_response(200) - m_delete.return_value = mock_response(200) + m_post.return_value = mock_response() + m_delete.return_value = mock_response() self.client.templates.create( data=self.post_template_data, domain=self.domain ) @@ -1224,7 +1224,7 @@ def test_post_version_template(self, m_post: MagicMock) -> None: @patch("mailgun.client.requests.get") @patch("mailgun.client.requests.post") def test_get_version_template(self, m_post: MagicMock, m_get: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_get.return_value = mock_response(200, {"template": {}}) req = self.client.templates.get( domain=self.domain, @@ -1237,7 +1237,7 @@ def test_get_version_template(self, m_post: MagicMock, m_get: MagicMock) -> None @patch("mailgun.client.requests.put") @patch("mailgun.client.requests.post") def test_put_version_template(self, m_post: MagicMock, m_put: MagicMock) -> None: - m_post.return_value = mock_response(200) + m_post.return_value = mock_response() m_put.return_value = mock_response(200, {"template": {}}) req = self.client.templates.put( domain=self.domain, @@ -1254,8 +1254,8 @@ def test_put_version_template(self, m_post: MagicMock, m_put: MagicMock) -> None def test_delete_version_template( self, m_post: MagicMock, m_delete: MagicMock ) -> None: - m_post.return_value = mock_response(200) - m_delete.return_value = mock_response(200) + m_post.return_value = mock_response() + m_delete.return_value = mock_response() self.client.templates.create( data=self.post_template_data, domain=self.domain ) @@ -1309,8 +1309,9 @@ def setUp(self) -> None: self.domain = DOMAIN now = datetime.now() now_formatted = now.strftime("%a, %d %b %Y %H:%M:%S +0000") - yesterday = now - timedelta(days=1) - yesterday_formatted = yesterday.strftime("%a, %d %b %Y %H:%M:%S +0000") + yesterday_formatted = (now - timedelta(days=1)).strftime( + "%a, %d %b %Y %H:%M:%S +0000" + ) self.account_metrics_data = { "start": yesterday_formatted, "end": now_formatted, @@ -1330,8 +1331,7 @@ def setUp(self) -> None: "include_subaccounts": True, "include_aggregates": True, } - self.invalid_account_metrics_data = { - **self.account_metrics_data, + self.invalid_account_metrics_data = self.account_metrics_data | { "resolution": "century", "duration": "1c", } @@ -1345,8 +1345,7 @@ def setUp(self) -> None: "include_subaccounts": True, "include_aggregates": True, } - self.invalid_account_usage_metrics_data = { - **self.account_usage_metrics_data, + self.invalid_account_usage_metrics_data = self.account_usage_metrics_data | { "resolution": "century", } @@ -1459,8 +1458,9 @@ def setUp(self) -> None: self.domain = DOMAIN now = datetime.now() now_formatted = now.strftime("%a, %d %b %Y %H:%M:%S +0000") - yesterday = now - timedelta(days=1) - yesterday_formatted = yesterday.strftime("%a, %d %b %Y %H:%M:%S +0000") + yesterday_formatted = (now - timedelta(days=1)).strftime( + "%a, %d %b %Y %H:%M:%S +0000" + ) self.account_logs_data = { "start": yesterday_formatted, "end": now_formatted, From 4406bc084cb129ba82226068dbcfba4e1a4e2923 Mon Sep 17 00:00:00 2001 From: Yaroslav Biletskiy Date: Wed, 4 Mar 2026 17:23:11 +0100 Subject: [PATCH 30/30] Added interrogate configuration --- .pre-commit-config.yaml | 2 +- pyproject.toml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3096874..f497bcb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -260,7 +260,7 @@ repos: hooks: - id: interrogate name: "๐Ÿ“ docs ยท Check docstring coverage" - args: [ --verbose, --fail-under=53, --ignore-init-method, --exclude "tests"] + args: [ --verbose] # Python type checking - repo: https://github.com/pre-commit/mirrors-mypy diff --git a/pyproject.toml b/pyproject.toml index ec2849a..1e4beb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -289,6 +289,11 @@ exclude_lines = [ "if TYPE_CHECKING:", ] +[tool.interrogate] +exclude = ["tests"] +fail-under = 53 +ignore-init-method = true + [tool.mypy] strict = true # Adapted from this StackOverflow post: