Skip to content

Commit c1be4d7

Browse files
Add sandbox support in MailtrapClient
Introduced `sandbox` mode, allowing configuration for sandbox environments via `inbox_id`. Implemented validation to ensure correct usage of `sandbox` and `inbox_id`, along with related tests. Updated URL generation logic and error handling for enhanced flexibility.
1 parent 8ca3c57 commit c1be4d7

File tree

4 files changed

+77
-4
lines changed

4 files changed

+77
-4
lines changed

mailtrap/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .client import MailtrapClient
22
from .exceptions import APIError
33
from .exceptions import AuthorizationError
4+
from .exceptions import ClientConfigurationError
45
from .exceptions import MailtrapError
56
from .mail import Address
67
from .mail import Attachment

mailtrap/client.py

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,40 @@
11
from typing import NoReturn
2+
from typing import Optional
23
from typing import Union
34

45
import requests
56

67
from mailtrap.exceptions import APIError
78
from mailtrap.exceptions import AuthorizationError
9+
from mailtrap.exceptions import ClientConfigurationError
810
from mailtrap.mail.base import BaseMail
911

1012

1113
class MailtrapClient:
1214
DEFAULT_HOST = "send.api.mailtrap.io"
1315
DEFAULT_PORT = 443
16+
SANDBOX_HOST = "sandbox.api.mailtrap.io"
1417

1518
def __init__(
1619
self,
1720
token: str,
18-
api_host: str = DEFAULT_HOST,
21+
api_host: Optional[str] = None,
1922
api_port: int = DEFAULT_PORT,
23+
sandbox: bool = False,
24+
inbox_id: Optional[str] = None,
2025
) -> None:
2126
self.token = token
2227
self.api_host = api_host
2328
self.api_port = api_port
29+
self.sandbox = sandbox
30+
self.inbox_id = inbox_id
31+
32+
self._validate_itself()
2433

2534
def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]:
26-
url = f"{self.base_url}/api/send"
27-
response = requests.post(url, headers=self.headers, json=mail.api_data)
35+
response = requests.post(
36+
self.api_send_url, headers=self.headers, json=mail.api_data
37+
)
2838

2939
if response.ok:
3040
data: dict[str, Union[bool, list[str]]] = response.json()
@@ -34,7 +44,15 @@ def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]:
3444

3545
@property
3646
def base_url(self) -> str:
37-
return f"https://{self.api_host.rstrip('/')}:{self.api_port}"
47+
return f"https://{self._host.rstrip('/')}:{self.api_port}"
48+
49+
@property
50+
def api_send_url(self) -> str:
51+
url = f"{self.base_url}/api/send"
52+
if self.sandbox and self.inbox_id:
53+
return f"{url}/{self.inbox_id}"
54+
55+
return url
3856

3957
@property
4058
def headers(self) -> dict[str, str]:
@@ -46,6 +64,14 @@ def headers(self) -> dict[str, str]:
4664
),
4765
}
4866

67+
@property
68+
def _host(self) -> str:
69+
if self.api_host:
70+
return self.api_host
71+
if self.sandbox:
72+
return self.SANDBOX_HOST
73+
return self.DEFAULT_HOST
74+
4975
@staticmethod
5076
def _handle_failed_response(response: requests.Response) -> NoReturn:
5177
status_code = response.status_code
@@ -55,3 +81,12 @@ def _handle_failed_response(response: requests.Response) -> NoReturn:
5581
raise AuthorizationError(data["errors"])
5682

5783
raise APIError(status_code, data["errors"])
84+
85+
def _validate_itself(self) -> None:
86+
if self.sandbox and not self.inbox_id:
87+
raise ClientConfigurationError("`inbox_id` is required for sandbox mode")
88+
89+
if not self.sandbox and self.inbox_id:
90+
raise ClientConfigurationError(
91+
"`inbox_id` is not allowed in non-sandbox mode"
92+
)

mailtrap/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ class MailtrapError(Exception):
22
pass
33

44

5+
class ClientConfigurationError(MailtrapError):
6+
def __init__(self, message: str) -> None:
7+
super().__init__(message)
8+
9+
510
class APIError(MailtrapError):
611
def __init__(self, status: int, errors: list[str]) -> None:
712
self.status = status

tests/unit/test_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,43 @@ def get_client(**kwargs: Any) -> mt.MailtrapClient:
3030
props = {"token": "fake_token", **kwargs}
3131
return mt.MailtrapClient(**props)
3232

33+
@pytest.mark.parametrize(
34+
"arguments",
35+
[
36+
{"sandbox": True},
37+
{"inbox_id": "12345"},
38+
],
39+
)
40+
def test_client_validation(self, arguments: dict[str, Any]) -> None:
41+
with pytest.raises(mt.ClientConfigurationError):
42+
self.get_client(**arguments)
43+
3344
def test_base_url_should_truncate_slash_from_host(self) -> None:
3445
client = self.get_client(api_host="example.send.com/", api_port=543)
3546

3647
assert client.base_url == "https://example.send.com:543"
3748

49+
@pytest.mark.parametrize(
50+
"arguments, expected_url",
51+
[
52+
({}, "https://send.api.mailtrap.io:443/api/send"),
53+
(
54+
{"api_host": "example.send.com", "api_port": 543},
55+
"https://example.send.com:543/api/send",
56+
),
57+
(
58+
{"sandbox": True, "inbox_id": "12345"},
59+
"https://sandbox.api.mailtrap.io:443/api/send/12345",
60+
),
61+
],
62+
)
63+
def test_api_send_url_should_return_default_sending_url(
64+
self, arguments: dict[str, Any], expected_url: str
65+
) -> None:
66+
client = self.get_client(**arguments)
67+
68+
assert client.api_send_url == expected_url
69+
3870
def test_headers_should_return_appropriate_dict(self) -> None:
3971
client = self.get_client()
4072

0 commit comments

Comments
 (0)