Skip to content

Commit 17fee2e

Browse files
committed
Add unit tests for get() function
Test HTTP-level behavior including: - ETag extraction from response headers - If-None-Match header sent when etag provided - 304 Not Modified response handling - Fallback when 304 has no ETag header - Error response handling (APIError) - Authorization and User-Agent headers - Timeout and URL construction
1 parent dce65fd commit 17fee2e

File tree

1 file changed

+179
-0
lines changed

1 file changed

+179
-0
lines changed

posthog/test/test_request.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
import requests
88

99
from posthog.request import (
10+
APIError,
1011
DatetimeSerializer,
12+
GetResponse,
1113
QuotaLimitError,
1214
batch_post,
1315
decide,
1416
determine_server_host,
17+
get,
1518
)
1619
from posthog.test.test_utils import TEST_API_KEY
1720

@@ -107,6 +110,182 @@ def test_normal_decide_response(self):
107110
self.assertEqual(response["featureFlags"], {"flag1": True})
108111

109112

113+
class TestGet(unittest.TestCase):
114+
"""Unit tests for the get() function HTTP-level behavior."""
115+
116+
@mock.patch("posthog.request._session.get")
117+
def test_get_returns_data_and_etag(self, mock_get):
118+
"""Test that get() returns GetResponse with data and etag from headers."""
119+
mock_response = requests.Response()
120+
mock_response.status_code = 200
121+
mock_response.headers["ETag"] = '"abc123"'
122+
mock_response._content = json.dumps(
123+
{"flags": [{"key": "test-flag"}]}
124+
).encode("utf-8")
125+
mock_get.return_value = mock_response
126+
127+
response = get("api_key", "/test-url", host="https://example.com")
128+
129+
self.assertIsInstance(response, GetResponse)
130+
self.assertEqual(response.data, {"flags": [{"key": "test-flag"}]})
131+
self.assertEqual(response.etag, '"abc123"')
132+
self.assertFalse(response.not_modified)
133+
134+
@mock.patch("posthog.request._session.get")
135+
def test_get_sends_if_none_match_header_when_etag_provided(self, mock_get):
136+
"""Test that If-None-Match header is sent when etag parameter is provided."""
137+
mock_response = requests.Response()
138+
mock_response.status_code = 200
139+
mock_response.headers["ETag"] = '"new-etag"'
140+
mock_response._content = json.dumps({"flags": []}).encode("utf-8")
141+
mock_get.return_value = mock_response
142+
143+
get("api_key", "/test-url", host="https://example.com", etag='"previous-etag"')
144+
145+
call_kwargs = mock_get.call_args[1]
146+
self.assertEqual(call_kwargs["headers"]["If-None-Match"], '"previous-etag"')
147+
148+
@mock.patch("posthog.request._session.get")
149+
def test_get_does_not_send_if_none_match_when_no_etag(self, mock_get):
150+
"""Test that If-None-Match header is not sent when no etag provided."""
151+
mock_response = requests.Response()
152+
mock_response.status_code = 200
153+
mock_response._content = json.dumps({"flags": []}).encode("utf-8")
154+
mock_get.return_value = mock_response
155+
156+
get("api_key", "/test-url", host="https://example.com")
157+
158+
call_kwargs = mock_get.call_args[1]
159+
self.assertNotIn("If-None-Match", call_kwargs["headers"])
160+
161+
@mock.patch("posthog.request._session.get")
162+
def test_get_handles_304_not_modified(self, mock_get):
163+
"""Test that 304 Not Modified response returns not_modified=True with no data."""
164+
mock_response = requests.Response()
165+
mock_response.status_code = 304
166+
mock_response.headers["ETag"] = '"unchanged-etag"'
167+
mock_get.return_value = mock_response
168+
169+
response = get(
170+
"api_key", "/test-url", host="https://example.com", etag='"unchanged-etag"'
171+
)
172+
173+
self.assertIsInstance(response, GetResponse)
174+
self.assertIsNone(response.data)
175+
self.assertEqual(response.etag, '"unchanged-etag"')
176+
self.assertTrue(response.not_modified)
177+
178+
@mock.patch("posthog.request._session.get")
179+
def test_get_304_without_etag_header_uses_request_etag(self, mock_get):
180+
"""Test that 304 response without ETag header falls back to request etag."""
181+
mock_response = requests.Response()
182+
mock_response.status_code = 304
183+
# Server doesn't return ETag header on 304
184+
mock_get.return_value = mock_response
185+
186+
response = get(
187+
"api_key", "/test-url", host="https://example.com", etag='"original-etag"'
188+
)
189+
190+
self.assertTrue(response.not_modified)
191+
self.assertEqual(response.etag, '"original-etag"')
192+
193+
@mock.patch("posthog.request._session.get")
194+
def test_get_200_without_etag_header(self, mock_get):
195+
"""Test that 200 response without ETag header returns None for etag."""
196+
mock_response = requests.Response()
197+
mock_response.status_code = 200
198+
mock_response._content = json.dumps({"flags": []}).encode("utf-8")
199+
# No ETag header
200+
mock_get.return_value = mock_response
201+
202+
response = get("api_key", "/test-url", host="https://example.com")
203+
204+
self.assertFalse(response.not_modified)
205+
self.assertIsNone(response.etag)
206+
self.assertEqual(response.data, {"flags": []})
207+
208+
@mock.patch("posthog.request._session.get")
209+
def test_get_error_response_raises_api_error(self, mock_get):
210+
"""Test that error responses raise APIError."""
211+
mock_response = requests.Response()
212+
mock_response.status_code = 401
213+
mock_response._content = json.dumps({"detail": "Unauthorized"}).encode("utf-8")
214+
mock_get.return_value = mock_response
215+
216+
with self.assertRaises(APIError) as ctx:
217+
get("bad_key", "/test-url", host="https://example.com")
218+
219+
self.assertEqual(ctx.exception.status, 401)
220+
self.assertEqual(ctx.exception.message, "Unauthorized")
221+
222+
@mock.patch("posthog.request._session.get")
223+
def test_get_sends_authorization_header(self, mock_get):
224+
"""Test that Authorization header is sent with Bearer token."""
225+
mock_response = requests.Response()
226+
mock_response.status_code = 200
227+
mock_response._content = json.dumps({}).encode("utf-8")
228+
mock_get.return_value = mock_response
229+
230+
get("my-api-key", "/test-url", host="https://example.com")
231+
232+
call_kwargs = mock_get.call_args[1]
233+
self.assertEqual(call_kwargs["headers"]["Authorization"], "Bearer my-api-key")
234+
235+
@mock.patch("posthog.request._session.get")
236+
def test_get_sends_user_agent_header(self, mock_get):
237+
"""Test that User-Agent header is sent."""
238+
mock_response = requests.Response()
239+
mock_response.status_code = 200
240+
mock_response._content = json.dumps({}).encode("utf-8")
241+
mock_get.return_value = mock_response
242+
243+
get("api_key", "/test-url", host="https://example.com")
244+
245+
call_kwargs = mock_get.call_args[1]
246+
self.assertIn("User-Agent", call_kwargs["headers"])
247+
self.assertTrue(call_kwargs["headers"]["User-Agent"].startswith("posthog-python/"))
248+
249+
@mock.patch("posthog.request._session.get")
250+
def test_get_passes_timeout(self, mock_get):
251+
"""Test that timeout parameter is passed to the request."""
252+
mock_response = requests.Response()
253+
mock_response.status_code = 200
254+
mock_response._content = json.dumps({}).encode("utf-8")
255+
mock_get.return_value = mock_response
256+
257+
get("api_key", "/test-url", host="https://example.com", timeout=30)
258+
259+
call_kwargs = mock_get.call_args[1]
260+
self.assertEqual(call_kwargs["timeout"], 30)
261+
262+
@mock.patch("posthog.request._session.get")
263+
def test_get_constructs_full_url(self, mock_get):
264+
"""Test that host and url are combined correctly."""
265+
mock_response = requests.Response()
266+
mock_response.status_code = 200
267+
mock_response._content = json.dumps({}).encode("utf-8")
268+
mock_get.return_value = mock_response
269+
270+
get("api_key", "/api/flags", host="https://example.com")
271+
272+
call_args = mock_get.call_args[0]
273+
self.assertEqual(call_args[0], "https://example.com/api/flags")
274+
275+
@mock.patch("posthog.request._session.get")
276+
def test_get_removes_trailing_slash_from_host(self, mock_get):
277+
"""Test that trailing slash is removed from host."""
278+
mock_response = requests.Response()
279+
mock_response.status_code = 200
280+
mock_response._content = json.dumps({}).encode("utf-8")
281+
mock_get.return_value = mock_response
282+
283+
get("api_key", "/api/flags", host="https://example.com/")
284+
285+
call_args = mock_get.call_args[0]
286+
self.assertEqual(call_args[0], "https://example.com/api/flags")
287+
288+
110289
@pytest.mark.parametrize(
111290
"host, expected",
112291
[

0 commit comments

Comments
 (0)