Skip to content

Commit 584762b

Browse files
authored
Merge pull request #81 from anvilco/absolute-url
Create class to handle absolute urls
2 parents 28e4d3f + e608a79 commit 584762b

4 files changed

Lines changed: 92 additions & 2 deletions

File tree

python_anvil/api.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
ForgeSubmitPayload,
1616
GeneratePDFPayload,
1717
)
18-
from .api_resources.requests import PlainRequest, RestRequest
18+
from .api_resources.requests import FullyQualifiedRequest, PlainRequest, RestRequest
1919
from .http import GQLClient, HTTPClient
2020

2121

@@ -117,6 +117,10 @@ def request_rest(self, options: Optional[dict] = None):
117117
api = RestRequest(self.client, options=options)
118118
return api
119119

120+
def request_fully_qualified(self, options: Optional[dict] = None):
121+
api = FullyQualifiedRequest(self.client, options=options)
122+
return api
123+
120124
def fill_pdf(
121125
self, template_id: str, payload: Union[dict, AnyStr, FillPDFPayload], **kwargs
122126
):

python_anvil/api_resources/requests.py

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

3+
from python_anvil.constants import VALID_HOSTS
34
from python_anvil.http import HTTPClient
45

56

@@ -161,3 +162,22 @@ class PlainRequest(BaseAnvilHttpRequest):
161162

162163
def get_url(self):
163164
return f"{self.API_HOST}/{self.API_BASE}"
165+
166+
167+
class FullyQualifiedRequest(BaseAnvilHttpRequest):
168+
"""A request class that validates URLs point to Anvil domains."""
169+
170+
def get_url(self):
171+
return "" # Not used since we expect full URLs
172+
173+
def _validate_url(self, url):
174+
if not any(url.startswith(host) for host in VALID_HOSTS):
175+
raise ValueError(f"URL must start with one of: {', '.join(VALID_HOSTS)}")
176+
177+
def get(self, url, params=None, **kwargs):
178+
self._validate_url(url)
179+
return super().get(url, params, **kwargs)
180+
181+
def post(self, url, data=None, **kwargs):
182+
self._validate_url(url)
183+
return super().post(url, data, **kwargs)

python_anvil/constants.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
"""Basic constants used in the library."""
22

33
GRAPHQL_ENDPOINT: str = "https://graphql.useanvil.com"
4-
REST_ENDPOINT = "https://app.useanvil.com/api/v1/"
4+
REST_ENDPOINT = "https://app.useanvil.com/api/v1"
5+
ANVIL_HOST = "https://app.useanvil.com"
6+
7+
VALID_HOSTS = [
8+
ANVIL_HOST,
9+
REST_ENDPOINT,
10+
GRAPHQL_ENDPOINT,
11+
]
512

613
RETRIES_LIMIT = 5
714
REQUESTS_LIMIT = {

python_anvil/tests/test_api.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
CreateEtchPacketPayload,
1010
ForgeSubmitPayload,
1111
)
12+
from python_anvil.constants import VALID_HOSTS
1213

1314
from ..api_resources.payload import FillPDFPayload
1415
from . import payloads
@@ -396,3 +397,61 @@ def test_minimum_valid_data_submission(m_request_post, anvil):
396397
anvil.forge_submit(payload=payload)
397398
assert m_request_post.call_count == 1
398399
assert _expected_data in m_request_post.call_args
400+
401+
def describe_rest_request_absolute_url_behavior():
402+
@pytest.mark.parametrize(
403+
"url, should_raise",
404+
[
405+
("some/relative/path", True),
406+
("https://external.example.com/full/path/file.pdf", True),
407+
*[(host + "/some-endpoint", False) for host in VALID_HOSTS],
408+
],
409+
)
410+
@mock.patch("python_anvil.api_resources.requests.AnvilRequest._request")
411+
def test_get_behavior(mock_request, anvil, url, should_raise):
412+
mock_request.return_value = (b"fake_content", 200, {})
413+
rest_client = anvil.request_fully_qualified()
414+
415+
if should_raise:
416+
with pytest.raises(
417+
ValueError,
418+
match="URL must start with one of: https://app.useanvil.com",
419+
):
420+
rest_client.get(url)
421+
else:
422+
rest_client.get(url)
423+
mock_request.assert_called_once_with(
424+
"GET",
425+
url,
426+
params=None,
427+
retry=True,
428+
)
429+
430+
@pytest.mark.parametrize(
431+
"url, should_raise",
432+
[
433+
("some/relative/path", True),
434+
("https://external.example.com/full/path/file.pdf", True),
435+
*[(host + "/some-endpoint", False) for host in VALID_HOSTS],
436+
],
437+
)
438+
@mock.patch("python_anvil.api_resources.requests.AnvilRequest._request")
439+
def test_post_behavior(mock_request, anvil, url, should_raise):
440+
mock_request.return_value = (b"fake_content", 200, {})
441+
rest_client = anvil.request_fully_qualified()
442+
443+
if should_raise:
444+
with pytest.raises(
445+
ValueError,
446+
match="URL must start with one of: https://app.useanvil.com",
447+
):
448+
rest_client.post(url, data={})
449+
else:
450+
rest_client.post(url, data={})
451+
mock_request.assert_called_once_with(
452+
"POST",
453+
url,
454+
json={},
455+
retry=True,
456+
params=None,
457+
)

0 commit comments

Comments
 (0)