Skip to content

Commit 8cd83b7

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 f93adc5 commit 8cd83b7

4 files changed

Lines changed: 78 additions & 4 deletions

File tree

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,32 +1,42 @@
11
from typing import Dict
22
from typing import List
33
from typing import NoReturn
4+
from typing import Optional
45
from typing import Union
56

67
import requests
78

89
from mailtrap.exceptions import APIError
910
from mailtrap.exceptions import AuthorizationError
11+
from mailtrap.exceptions import ClientConfigurationError
1012
from mailtrap.mail.base import BaseMail
1113

1214

1315
class MailtrapClient:
1416
DEFAULT_HOST = "send.api.mailtrap.io"
1517
DEFAULT_PORT = 443
18+
SANDBOX_HOST = "sandbox.api.mailtrap.io"
1619

1720
def __init__(
1821
self,
1922
token: str,
20-
api_host: str = DEFAULT_HOST,
23+
api_host: Optional[str] = None,
2124
api_port: int = DEFAULT_PORT,
25+
sandbox: bool = False,
26+
inbox_id: Optional[str] = None,
2227
) -> None:
2328
self.token = token
2429
self.api_host = api_host
2530
self.api_port = api_port
31+
self.sandbox = sandbox
32+
self.inbox_id = inbox_id
33+
34+
self._validate_itself()
2635

2736
def send(self, mail: BaseMail) -> Dict[str, Union[bool, List[str]]]:
28-
url = f"{self.base_url}/api/send"
29-
response = requests.post(url, headers=self.headers, json=mail.api_data)
37+
response = requests.post(
38+
self.api_send_url, headers=self.headers, json=mail.api_data
39+
)
3040

3141
if response.ok:
3242
data = response.json() # type: Dict[str, Union[bool, List[str]]]
@@ -36,7 +46,15 @@ def send(self, mail: BaseMail) -> Dict[str, Union[bool, List[str]]]:
3646

3747
@property
3848
def base_url(self) -> str:
39-
return f"https://{self.api_host.rstrip('/')}:{self.api_port}"
49+
return f"https://{self._host.rstrip('/')}:{self.api_port}"
50+
51+
@property
52+
def api_send_url(self) -> str:
53+
url = f"{self.base_url}/api/send"
54+
if self.sandbox and self.inbox_id:
55+
return f"{url}/{self.inbox_id}"
56+
57+
return url
4058

4159
@property
4260
def headers(self) -> Dict[str, str]:
@@ -48,6 +66,14 @@ def headers(self) -> Dict[str, str]:
4866
),
4967
}
5068

69+
@property
70+
def _host(self) -> str:
71+
if self.api_host:
72+
return self.api_host
73+
if self.sandbox:
74+
return self.SANDBOX_HOST
75+
return self.DEFAULT_HOST
76+
5177
@staticmethod
5278
def _handle_failed_response(response: requests.Response) -> NoReturn:
5379
status_code = response.status_code
@@ -57,3 +83,12 @@ def _handle_failed_response(response: requests.Response) -> NoReturn:
5783
raise AuthorizationError(data["errors"])
5884

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

mailtrap/exceptions.py

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

77

8+
class ClientConfigurationError(MailtrapError):
9+
def __init__(self, message: str) -> None:
10+
super().__init__(message)
11+
12+
813
class APIError(MailtrapError):
914
def __init__(self, status: int, errors: List[str]) -> None:
1015
self.status = status

tests/unit/test_client.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
22
from typing import Any
3+
from typing import Dict
34

45
import pytest
56
import responses
@@ -30,11 +31,43 @@ def get_client(**kwargs: Any) -> mt.MailtrapClient:
3031
props = {"token": "fake_token", **kwargs}
3132
return mt.MailtrapClient(**props)
3233

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

3648
assert client.base_url == "https://example.send.com:543"
3749

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

0 commit comments

Comments
 (0)