-
-
Notifications
You must be signed in to change notification settings - Fork 489
Expand file tree
/
Copy pathtest_http_send.py
More file actions
267 lines (198 loc) · 9.65 KB
/
test_http_send.py
File metadata and controls
267 lines (198 loc) · 9.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import os
from typing import Union
from unittest.mock import AsyncMock, patch
import httpx
import pytest
from dotenv import load_dotenv
from realtime import AsyncRealtimeChannel, AsyncRealtimeClient
load_dotenv()
URL = os.getenv("SUPABASE_URL") or "http://127.0.0.1:54321"
ANON_KEY = (
os.getenv("SUPABASE_ANON_KEY")
or "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"
)
@pytest.fixture
def socket() -> AsyncRealtimeClient:
url = f"{URL}/realtime/v1"
key = ANON_KEY
return AsyncRealtimeClient(url, key)
def create_mock_response(
status_code: int,
reason_phrase: str = "OK",
body: Union[dict[str, str], None] = None,
):
"""Create a mock HTTP response."""
from unittest.mock import Mock
mock_response = Mock()
mock_response.status_code = status_code
mock_response.reason_phrase = reason_phrase
if body:
mock_response.json = Mock(return_value=body)
else:
mock_response.json = Mock(side_effect=Exception("No JSON body"))
return mock_response
@pytest.mark.asyncio
async def test_http_send_without_access_token():
"""Test http_send with no access token."""
# Create a client without setting access_token
url = f"{URL}/realtime/v1"
socket_no_token = AsyncRealtimeClient(url, None)
channel: AsyncRealtimeChannel = socket_no_token.channel("test-topic")
mock_response = create_mock_response(202, "Accepted")
with patch("httpx.AsyncClient.post", return_value=mock_response) as mock_post:
result = await channel.http_send("test-event", {"data": "test"})
assert result == {"success": True}
assert mock_post.called
call_args = mock_post.call_args
# Verify headers
headers = call_args.kwargs["headers"]
assert headers["Authorization"] == ""
assert headers["apikey"] == ""
assert headers["Content-Type"] == "application/json"
# Verify body
body = call_args.kwargs["json"]
assert body["messages"][0]["topic"] == "realtime:test-topic"
assert body["messages"][0]["event"] == "test-event"
assert body["messages"][0]["payload"] == {"data": "test"}
assert body["messages"][0]["private"] is False
@pytest.mark.asyncio
async def test_http_send_with_access_token(socket: AsyncRealtimeClient):
"""Test http_send with access token."""
await socket.set_auth("token123")
channel: AsyncRealtimeChannel = socket.channel("test-topic")
mock_response = create_mock_response(202, "Accepted")
with patch("httpx.AsyncClient.post", return_value=mock_response) as mock_post:
result = await channel.http_send("test-event", {"data": "test"})
assert result == {"success": True}
assert mock_post.called
call_args = mock_post.call_args
# Verify Authorization header includes token
headers = call_args.kwargs["headers"]
assert headers["Authorization"] == "Bearer token123"
assert headers["apikey"] == ANON_KEY
@pytest.mark.asyncio
async def test_http_send_rejects_when_payload_is_none(socket: AsyncRealtimeClient):
"""Test http_send raises ValueError when payload is None."""
channel: AsyncRealtimeChannel = socket.channel("test-topic")
with pytest.raises(ValueError, match="Payload is required for http_send"):
await channel.http_send("test-event", None)
@pytest.mark.asyncio
async def test_http_send_handles_timeout_error(socket: AsyncRealtimeClient):
"""Test http_send handles timeout errors."""
channel: AsyncRealtimeChannel = socket.channel("test-topic")
with patch(
"httpx.AsyncClient.post", side_effect=httpx.TimeoutException("Request timeout")
):
with pytest.raises(Exception, match="Request timeout"):
await channel.http_send("test-event", {"data": "test"})
@pytest.mark.asyncio
async def test_http_send_handles_non_202_status(socket: AsyncRealtimeClient):
"""Test http_send handles non-202 status codes."""
channel: AsyncRealtimeChannel = socket.channel("test-topic")
mock_response = create_mock_response(
500, "Internal Server Error", {"error": "Server error"}
)
with patch("httpx.AsyncClient.post", return_value=mock_response):
with pytest.raises(Exception, match="Server error"):
await channel.http_send("test-event", {"data": "test"})
@pytest.mark.asyncio
async def test_http_send_uses_error_message_from_body(socket: AsyncRealtimeClient):
"""Test http_send uses error message from response body."""
channel: AsyncRealtimeChannel = socket.channel("test-topic")
mock_response = create_mock_response(
400, "Bad Request", {"message": "Invalid request"}
)
with patch("httpx.AsyncClient.post", return_value=mock_response):
with pytest.raises(Exception, match="Invalid request"):
await channel.http_send("test-event", {"data": "test"})
@pytest.mark.asyncio
async def test_http_send_falls_back_to_reason_phrase(socket: AsyncRealtimeClient):
"""Test http_send falls back to reason phrase when JSON parsing fails."""
channel: AsyncRealtimeChannel = socket.channel("test-topic")
mock_response = create_mock_response(503, "Service Unavailable")
with patch("httpx.AsyncClient.post", return_value=mock_response):
with pytest.raises(Exception, match="Service Unavailable"):
await channel.http_send("test-event", {"data": "test"})
@pytest.mark.asyncio
async def test_http_send_respects_custom_timeout(socket: AsyncRealtimeClient):
"""Test http_send respects custom timeout option."""
channel: AsyncRealtimeChannel = socket.channel("test-topic")
mock_response = create_mock_response(202, "Accepted")
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_client.post.return_value = mock_response
mock_client_class.return_value = mock_client
await channel.http_send("test-event", {"data": "test"}, timeout=3000)
# Verify timeout was passed correctly (3000ms = 3.0s)
assert mock_client_class.called
call_args = mock_client_class.call_args
assert call_args.kwargs["timeout"] == 3.0
@pytest.mark.asyncio
async def test_http_send_with_private_channel(socket: AsyncRealtimeClient):
"""Test http_send with a private channel."""
channel: AsyncRealtimeChannel = socket.channel(
"test-topic",
params={"config": {"private": True, "broadcast": None, "presence": None}},
)
mock_response = create_mock_response(202, "Accepted")
with patch("httpx.AsyncClient.post", return_value=mock_response) as mock_post:
result = await channel.http_send("test-event", {"data": "test"})
assert result == {"success": True}
assert mock_post.called
# Verify private flag is set
body = mock_post.call_args.kwargs["json"]
assert body["messages"][0]["private"] is True
@pytest.mark.asyncio
async def test_http_send_uses_default_timeout(socket: AsyncRealtimeClient):
"""Test http_send uses default timeout when not specified."""
socket_with_custom_timeout = AsyncRealtimeClient(
f"{URL}/realtime/v1", ANON_KEY, timeout=5000
)
channel: AsyncRealtimeChannel = socket_with_custom_timeout.channel("test-topic")
mock_response = create_mock_response(202, "Accepted")
with patch("httpx.AsyncClient") as mock_client_class:
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_client.post.return_value = mock_response
mock_client_class.return_value = mock_client
await channel.http_send("test-event", {"data": "test"})
# Verify default timeout was used (5000ms = 5.0s)
assert mock_client_class.called
call_args = mock_client_class.call_args
assert call_args.kwargs["timeout"] == 5.0
@pytest.mark.asyncio
async def test_http_send_sends_correct_payload(socket: AsyncRealtimeClient):
"""Test http_send sends the correct payload structure."""
channel: AsyncRealtimeChannel = socket.channel("test-topic")
mock_response = create_mock_response(202, "Accepted")
with patch("httpx.AsyncClient.post", return_value=mock_response) as mock_post:
test_payload = {"key": "value", "nested": {"data": 123}}
result = await channel.http_send("test-payload-event", test_payload)
assert result == {"success": True}
assert mock_post.called
# Verify the exact payload structure
body = mock_post.call_args.kwargs["json"]
assert body["messages"][0]["topic"] == "realtime:test-topic"
assert body["messages"][0]["event"] == "test-payload-event"
assert body["messages"][0]["payload"] == test_payload
@pytest.mark.asyncio
async def test_send_broadcast_shows_warning_when_not_connected(
socket: AsyncRealtimeClient, caplog
):
"""Test send_broadcast shows deprecation warning when not connected."""
channel: AsyncRealtimeChannel = socket.channel("test-topic")
# Don't connect the socket, so _can_push() returns False
# This will trigger the warning
with pytest.raises(Exception):
# send_broadcast will fail because we're not subscribed, but we want to check the warning
await channel.send_broadcast("test-event", {"data": "test"})
# Check that the warning was logged
warning_found = any(
"falling back to REST API" in record.message
for record in caplog.records
if record.levelname == "WARNING"
)
assert warning_found, "Expected deprecation warning was not logged"