Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion python_anvil/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
ForgeSubmitPayload,
GeneratePDFPayload,
)
from .api_resources.requests import PlainRequest, RestRequest
from .api_resources.requests import FullyQualifiedRequest, PlainRequest, RestRequest
from .http import GQLClient, HTTPClient


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

def request_fully_qualified(self, options: Optional[dict] = None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idt this is actually used anywhere - looks like the func above is used here. a couple other placess too, probably should use this new version

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, request_rest also doesnt seem to be called anywhere. I guess someone could use these if they wanted to when making other requests to Anvil

api = FullyQualifiedRequest(self.client, options=options)
return api

def fill_pdf(
self, template_id: str, payload: Union[dict, AnyStr, FillPDFPayload], **kwargs
):
Expand Down
26 changes: 26 additions & 0 deletions python_anvil/api_resources/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,29 @@ class PlainRequest(BaseAnvilHttpRequest):

def get_url(self):
return f"{self.API_HOST}/{self.API_BASE}"


class FullyQualifiedRequest(BaseAnvilHttpRequest):
"""A request class that validates URLs point to Anvil domains."""

VALID_HOSTS = [
"https://app.useanvil.com",
# Future Anvil specific URLs
]

def get_url(self):
return "" # Not used since we expect full URLs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to define this? the base of this func throws an error, prob can keep if we don't want people to use this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is intentionally defined as an empty string, otherwise request_rest tries to append whatever URL is passed in to whatever is returned in get_url

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if that's the case then yeah def, but doesn't this function get inherited from here? I'm not a python guy so idk how inheritance works here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does get inherited from there but we are overriding it here by returning an empty string


def _validate_url(self, url):
if not any(url.startswith(host) for host in self.VALID_HOSTS):
raise ValueError(
f"URL must start with one of: {', '.join(self.VALID_HOSTS)}"
)

def get(self, url, params=None, **kwargs):
self._validate_url(url)
return super().get(url, params, **kwargs)

def post(self, url, data=None, **kwargs):
self._validate_url(url)
return super().post(url, data, **kwargs)
58 changes: 58 additions & 0 deletions python_anvil/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,61 @@ def test_minimum_valid_data_submission(m_request_post, anvil):
anvil.forge_submit(payload=payload)
assert m_request_post.call_count == 1
assert _expected_data in m_request_post.call_args

def describe_rest_request_absolute_url_behavior():
@pytest.mark.parametrize(
"url, should_raise",
[
("some/relative/path", True),
("https://external.example.com/full/path/file.pdf", True),
("https://app.useanvil.com/api/v1/some-endpoint", False),
],
)
@mock.patch("python_anvil.api_resources.requests.AnvilRequest._request")
def test_get_behavior(mock_request, anvil, url, should_raise):
mock_request.return_value = (b"fake_content", 200, {})
rest_client = anvil.request_fully_qualified()

if should_raise:
with pytest.raises(
ValueError,
match="URL must start with one of: https://app.useanvil.com",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this uses VALID_HOSTS somehow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

):
rest_client.get(url)
else:
rest_client.get(url)
mock_request.assert_called_once_with(
"GET",
url,
params=None,
retry=True,
)

@pytest.mark.parametrize(
"url, should_raise",
[
("some/relative/path", True),
("https://external.example.com/full/path/file.pdf", True),
("https://app.useanvil.com/api/v1/some-endpoint", False),
],
)
@mock.patch("python_anvil.api_resources.requests.AnvilRequest._request")
def test_post_behavior(mock_request, anvil, url, should_raise):
mock_request.return_value = (b"fake_content", 200, {})
rest_client = anvil.request_fully_qualified()

if should_raise:
with pytest.raises(
ValueError,
match="URL must start with one of: https://app.useanvil.com",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally this uses VALID_HOSTS somehow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

):
rest_client.post(url, data={})
else:
rest_client.post(url, data={})
mock_request.assert_called_once_with(
"POST",
url,
json={},
retry=True,
params=None,
)