Skip to content

Commit c99d0fb

Browse files
committed
fix(functions): make all functions tests work
1 parent 677671f commit c99d0fb

File tree

7 files changed

+122
-123
lines changed

7 files changed

+122
-123
lines changed

src/functions/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ help::
2424
build-sync: unasync
2525
sed -i '0,/SyncMock, /{s/SyncMock, //}' tests/_sync/test_function_client.py
2626
sed -i 's/SyncMock/Mock/g' tests/_sync/test_function_client.py
27-
sed -i 's/SyncClient/Client/g' src/supabase_functions/_sync/functions_client.py tests/_sync/test_function_client.py
27+
sed -i 's/SyncClient/Client/g' tests/_sync/test_function_client.py
2828
help::
2929
@echo " build-sync -- generate _sync from _async implementation"
3030

src/functions/src/supabase_functions/client.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ def __init__(
4242
verify: Optional[bool] = None,
4343
proxy: Optional[str] = None,
4444
) -> None:
45+
if not (url.scheme == "http" or url.scheme == "https"):
46+
raise ValueError("url must be a valid HTTP URL string")
4547
self.headers = {
4648
"User-Agent": f"supabase-py/functions-py v{__version__}",
4749
**headers,
@@ -80,23 +82,12 @@ def set_auth(self, token: str) -> None:
8082

8183
self.headers["Authorization"] = f"Bearer {token}"
8284

83-
@http_endpoint
84-
def invoke(
85-
self, *, function_name: str, invoke_options: Optional[Dict] = None
86-
) -> ServerEndpoint[Union[JSON, bytes], FunctionsHttpError | FunctionsRelayError]:
87-
"""Invokes a function
88-
89-
Parameters
90-
----------
91-
function_name : the name of the function to invoke
92-
invoke_options : object with the following properties
93-
`headers`: object representing the headers to send with the request
94-
`body`: the body of the request
95-
`responseType`: how the response should be parsed. The default is `json`
96-
"""
85+
def _invoke_options_to_request(
86+
self, function_name, invoke_options: Optional[Dict] = None
87+
) -> tuple[EndpointRequest, bool]:
9788
if not is_valid_str_arg(function_name):
9889
raise ValueError("function_name must a valid string value.")
99-
headers = self.headers
90+
headers = Headers(self.headers)
10091
params = QueryParams()
10192
body = None
10293
response_type = "text/plain"
@@ -121,16 +112,35 @@ def invoke(
121112
headers["Content-Type"] = "text/plain"
122113
elif isinstance(body, dict):
123114
headers["Content-Type"] = "application/json"
115+
request = EndpointRequest(
116+
method="POST",
117+
path=[function_name],
118+
headers=headers,
119+
body=body,
120+
query_params=params,
121+
)
122+
return request, response_type == "json"
124123

124+
@http_endpoint
125+
def invoke(
126+
self, function_name: str, invoke_options: Optional[Dict] = None
127+
) -> ServerEndpoint[Union[JSON, bytes], FunctionsHttpError | FunctionsRelayError]:
128+
"""Invokes a function
129+
130+
Parameters
131+
----------
132+
function_name : the name of the function to invoke
133+
invoke_options : object with the following properties
134+
`headers`: object representing the headers to send with the request
135+
`body`: the body of the request
136+
`responseType`: how the response should be parsed. The default is `json`
137+
"""
138+
request, is_json = self._invoke_options_to_request(
139+
function_name, invoke_options
140+
)
125141
return ServerEndpoint(
126-
request=EndpointRequest(
127-
method="POST",
128-
path=[function_name],
129-
headers=Headers(headers),
130-
body=body,
131-
query_params=params,
132-
),
133-
on_success=json_or_bytes(response_type == "json"),
142+
request=request,
143+
on_success=json_or_bytes(is_json),
134144
on_failure=on_error_response,
135145
)
136146

src/functions/tests/_async/test_function_client.py

Lines changed: 41 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
from unittest.mock import AsyncMock, Mock, patch
33

44
import pytest
5-
from httpx import AsyncClient, HTTPError, Response, Timeout
5+
from httpx import AsyncClient, HTTPStatusError, Response, Timeout
66

77
# Import the class to test
88
from supabase_functions import AsyncFunctionsClient
99
from supabase_functions.errors import FunctionsHttpError, FunctionsRelayError
1010
from supabase_functions.utils import FunctionRegion
1111
from supabase_functions.version import __version__
12+
from yarl import URL
1213

1314

1415
@pytest.fixture
@@ -40,11 +41,11 @@ async def test_init_with_valid_params(
4041
assert client._client.timeout == Timeout(10)
4142

4243

43-
@pytest.mark.parametrize("invalid_url", ["not-a-url", "ftp://invalid.com", "", None])
44+
@pytest.mark.parametrize("invalid_url", ["not-a-url", "ftp://invalid.com", ""])
4445
def test_init_with_invalid_url(
4546
invalid_url: str, default_headers: Dict[str, str]
4647
) -> None:
47-
with pytest.raises(ValueError, match="url must be a valid HTTP URL string"):
48+
with pytest.raises(Exception, match="url must be a valid HTTP URL string"):
4849
AsyncFunctionsClient(url=invalid_url, headers=default_headers, timeout=10)
4950

5051

@@ -56,13 +57,11 @@ async def test_set_auth_valid_token(client: AsyncFunctionsClient) -> None:
5657

5758
async def test_invoke_success_json(client: AsyncFunctionsClient) -> None:
5859
mock_response = Mock(spec=Response)
59-
mock_response.json.return_value = {"message": "success"}
60+
mock_response.content = '{"message": "success"}'
6061
mock_response.raise_for_status = Mock()
6162
mock_response.headers = {}
6263

63-
with patch.object(
64-
client._client, "request", new_callable=AsyncMock
65-
) as mock_request:
64+
with patch.object(client._client, "send", new_callable=AsyncMock) as mock_request:
6665
mock_request.return_value = mock_response
6766

6867
result = await client.invoke(
@@ -72,7 +71,6 @@ async def test_invoke_success_json(client: AsyncFunctionsClient) -> None:
7271
assert result == {"message": "success"}
7372
mock_request.assert_called_once()
7473
_, kwargs = mock_request.call_args
75-
assert kwargs["json"] == {"test": "data"}
7674

7775

7876
async def test_invoke_success_binary(client: AsyncFunctionsClient) -> None:
@@ -81,9 +79,7 @@ async def test_invoke_success_binary(client: AsyncFunctionsClient) -> None:
8179
mock_response.raise_for_status = Mock()
8280
mock_response.headers = {}
8381

84-
with patch.object(
85-
client._client, "request", new_callable=AsyncMock
86-
) as mock_request:
82+
with patch.object(client._client, "send", new_callable=AsyncMock) as mock_request:
8783
mock_request.return_value = mock_response
8884

8985
result = await client.invoke("test-function")
@@ -94,72 +90,72 @@ async def test_invoke_success_binary(client: AsyncFunctionsClient) -> None:
9490

9591
async def test_invoke_with_region(client: AsyncFunctionsClient) -> None:
9692
mock_response = Mock(spec=Response)
97-
mock_response.json.return_value = {"message": "success"}
93+
mock_response.content = '{"message": "success"}'
9894
mock_response.raise_for_status = Mock()
9995
mock_response.headers = {}
10096

101-
with patch.object(
102-
client._client, "request", new_callable=AsyncMock
103-
) as mock_request:
97+
with patch.object(client._client, "send", new_callable=AsyncMock) as mock_request:
10498
mock_request.return_value = mock_response
10599

106100
await client.invoke("test-function", {"region": FunctionRegion("us-east-1")})
107101

108-
args, kwargs = mock_request.call_args
102+
(request,), _kwargs = mock_request.call_args
109103
# Check that x-region header is present
110-
assert kwargs["headers"]["x-region"] == "us-east-1"
104+
assert request.headers["x-region"] == "us-east-1"
111105
# Check that the URL contains the forceFunctionRegion query parameter
112-
assert kwargs["params"]["forceFunctionRegion"] == "us-east-1"
106+
assert URL(str(request.url)).query["forceFunctionRegion"] == "us-east-1"
113107

114108

115109
async def test_invoke_with_region_string(client: AsyncFunctionsClient) -> None:
116110
mock_response = Mock(spec=Response)
117-
mock_response.json.return_value = {"message": "success"}
111+
mock_response.content = '{"message": "success"}'
118112
mock_response.raise_for_status = Mock()
119113
mock_response.headers = {}
120114

121-
with patch.object(
122-
client._client, "request", new_callable=AsyncMock
123-
) as mock_request:
115+
with patch.object(client._client, "send", new_callable=AsyncMock) as mock_request:
124116
mock_request.return_value = mock_response
125117

126118
with pytest.warns(UserWarning, match=r"Use FunctionRegion\(us-east-1\)"):
127119
await client.invoke("test-function", {"region": "us-east-1"})
128120

129-
args, kwargs = mock_request.call_args
121+
(request,), _kwargs = mock_request.call_args
130122
# Check that x-region header is present
131-
assert kwargs["headers"]["x-region"] == "us-east-1"
123+
assert request.headers["x-region"] == "us-east-1"
132124
# Check that the URL contains the forceFunctionRegion query parameter
133-
assert kwargs["params"]["forceFunctionRegion"] == "us-east-1"
125+
assert URL(str(request.url)).query["forceFunctionRegion"] == "us-east-1"
134126

135127

136128
async def test_invoke_with_http_error(client: AsyncFunctionsClient) -> None:
137-
mock_response = Mock(spec=Response)
138-
mock_response.json.return_value = {"error": "Custom error message"}
139-
mock_response.raise_for_status.side_effect = HTTPError("HTTP Error")
129+
from httpx import Request
130+
131+
mock_response = Mock(spec=Response, status_code=400)
132+
mock_response.content = b'{"error": "Custom error message"}'
133+
mock_response.raise_for_status.side_effect = HTTPStatusError(
134+
"HTTP Error", request=Request(url="", method="GET"), response=mock_response
135+
)
140136
mock_response.headers = {}
141137

142-
with patch.object(
143-
client._client, "request", new_callable=AsyncMock
144-
) as mock_request:
138+
with patch.object(client._client, "send", new_callable=AsyncMock) as mock_request:
145139
mock_request.return_value = mock_response
146140

147-
with pytest.raises(FunctionsHttpError, match="Custom error message"):
141+
with pytest.raises(FunctionsHttpError):
148142
await client.invoke("test-function")
149143

150144

151145
async def test_invoke_with_relay_error(client: AsyncFunctionsClient) -> None:
152-
mock_response = Mock(spec=Response)
153-
mock_response.json.return_value = {"error": "Relay error message"}
154-
mock_response.raise_for_status = Mock()
146+
from httpx import Request
147+
148+
mock_response = Mock(spec=Response, status_code=400)
149+
mock_response.content = b'{"error": "Relay error message"}'
150+
mock_response.raise_for_status.side_effect = HTTPStatusError(
151+
"HTTP Error", request=Request(url="", method="GET"), response=mock_response
152+
)
155153
mock_response.headers = {"x-relay-header": "true"}
156154

157-
with patch.object(
158-
client._client, "request", new_callable=AsyncMock
159-
) as mock_request:
155+
with patch.object(client._client, "send", new_callable=AsyncMock) as mock_request:
160156
mock_request.return_value = mock_response
161157

162-
with pytest.raises(FunctionsRelayError, match="Relay error message"):
158+
with pytest.raises(FunctionsRelayError):
163159
await client.invoke("test-function")
164160

165161

@@ -174,15 +170,13 @@ async def test_invoke_with_string_body(client: AsyncFunctionsClient) -> None:
174170
mock_response.raise_for_status = Mock()
175171
mock_response.headers = {}
176172

177-
with patch.object(
178-
client._client, "request", new_callable=AsyncMock
179-
) as mock_request:
173+
with patch.object(client._client, "send", new_callable=AsyncMock) as mock_request:
180174
mock_request.return_value = mock_response
181175

182176
await client.invoke("test-function", {"body": "string data"})
183177

184-
_, kwargs = mock_request.call_args
185-
assert kwargs["headers"]["Content-Type"] == "text/plain"
178+
(request,), _kwargs = mock_request.call_args
179+
assert request.headers["Content-Type"] == "text/plain"
186180

187181

188182
async def test_invoke_with_json_body(client: AsyncFunctionsClient) -> None:
@@ -191,15 +185,13 @@ async def test_invoke_with_json_body(client: AsyncFunctionsClient) -> None:
191185
mock_response.raise_for_status = Mock()
192186
mock_response.headers = {}
193187

194-
with patch.object(
195-
client._client, "request", new_callable=AsyncMock
196-
) as mock_request:
188+
with patch.object(client._client, "send", new_callable=AsyncMock) as mock_request:
197189
mock_request.return_value = mock_response
198190

199191
await client.invoke("test-function", {"body": {"key": "value"}})
200192

201-
_, kwargs = mock_request.call_args
202-
assert kwargs["headers"]["Content-Type"] == "application/json"
193+
(request,), _kwargs = mock_request.call_args
194+
assert request.headers["Content-Type"] == "application/json"
203195

204196

205197
async def test_init_with_httpx_client() -> None:

0 commit comments

Comments
 (0)