-
Notifications
You must be signed in to change notification settings - Fork 4
Refactored test framework #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
erz9engel
wants to merge
30
commits into
main
Choose a base branch
from
DE-1684/test_framework_refactoring
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
fb74094
Moved existing tests to integration
erz9engel 3015b32
Created unit tests folder
erz9engel 770d073
Ported MessagesTests
erz9engel 862bf97
Ported DomainTests
erz9engel 93490d3
Ported IpTests
erz9engel 170d756
Ported IpPoolTests
erz9engel 7a7cb67
Ported EventsTests
erz9engel 2953f9b
Ported EventsTests
erz9engel 1d3fd32
Ported BouncesTests
erz9engel 83cfc78
Ported UnsubscribesTests
erz9engel c748c6b
Ported ComplaintsTests
erz9engel 46026a4
Ported WhiteListTests
erz9engel 472a339
Ported RoutesTests
erz9engel ef8430d
Ported WebhooksTests
erz9engel 4a2740a
Ported MailingListsTests
erz9engel a88fa07
Ported TemplatesTests
erz9engel c300b27
Ported MetricsTests
erz9engel 5318a51
Ported LogsTests
erz9engel f5f2aa6
Ported TagsNewTests
erz9engel 7634104
Ported BounceClassificationTests
erz9engel 5f3b083
Ported UsersTests
erz9engel b9202f4
Ported KeysTests
erz9engel 6be723d
Updated Makefile
erz9engel dd8b8d9
Added unit tests for handlers
erz9engel 9b3cc6a
Added unit tests for clients
erz9engel b00982e
Added unit tests to improve coverage
erz9engel a09bf17
Added unit tests to improve coverage
erz9engel 321519c
Skip false positive in tests
erz9engel fe56a9a
Linter fixes
erz9engel 4406bc0
Added interrogate configuration
erz9engel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Integration tests for the Mailgun API client.""" |
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Unit tests for the Mailgun API client.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, 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, 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: | ||
| 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"}' | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check failure
Code scanning / CodeQL
Incomplete URL substring sanitization High test
Copilot Autofix
AI 1 day ago
In general, to avoid incomplete URL substring sanitization, the code should parse the URL and check the host (and possibly path) using a URL parser rather than using
"example.com" in urlorurl.endswith("example.com"). In tests, this means asserting on parsed components instead of generic substring presence.Here, the best fix is to change the test so that it asserts that the hostname of the built URL equals the expected domain (
example.com), rather than that"example.com"appears somewhere in the full URL string. We can do this by importingurlparsefrom Python’s standard libraryurllib.parseand using it to extract the hostname fromresult.Concretely:
tests/unit/test_client.py:from urllib.parse import urlparsetest_build_url_domains_with_domain:assert "example.com" in resultwith parsing the URL and assertingurlparse(result).hostname == "example.com"(or at least that the hostname equals the expected value). This keeps the functional intent (verifying that the domain is correctly embedded) but in a more precise and secure way.