Skip to content

Commit 432021a

Browse files
author
Dylan Huang
committed
update
1 parent 1a14b73 commit 432021a

2 files changed

Lines changed: 243 additions & 4 deletions

File tree

eval_protocol/evaluation.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -800,11 +800,11 @@ def create(self, evaluator_id, display_name=None, description=None, force=False)
800800
tar_size = self._create_tar_gz_with_ignores(tar_path, cwd)
801801

802802
# Call GetEvaluatorUploadEndpoint
803-
upload_endpoint_url = f"{self.api_base}/v1/{evaluator_name}:getUploadEndpoint"
803+
upload_endpoint_path = f"v1/{evaluator_name}:getUploadEndpoint"
804804
upload_payload = {"name": evaluator_name, "filename_to_size": {tar_filename: tar_size}}
805805

806806
logger.info(f"Requesting upload endpoint for {tar_filename}")
807-
upload_response = client.post(upload_endpoint_url, json=upload_payload)
807+
upload_response = client.post(upload_endpoint_path, json=upload_payload)
808808
upload_response.raise_for_status()
809809

810810
# Check for signed URLs
@@ -884,9 +884,9 @@ def create(self, evaluator_id, display_name=None, description=None, force=False)
884884
raise
885885

886886
# Step 3: Validate upload
887-
validate_url = f"{self.api_base}/v1/{evaluator_name}:validateUpload"
887+
validate_path = f"v1/{evaluator_name}:validateUpload"
888888
validate_payload = {"name": evaluator_name}
889-
validate_response = client.post(validate_url, json=validate_payload)
889+
validate_response = client.post(validate_path, json=validate_payload)
890890
validate_response.raise_for_status()
891891

892892
validate_data = validate_response.json()

tests/test_fireworks_api_client.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,5 +225,244 @@ def test_user_agent_without_api_key(self):
225225
assert "Authorization" not in headers
226226

227227

228+
class TestFireworksAPIClientPathHandling:
229+
"""Test that FireworksAPIClient correctly handles relative paths and prevents URL construction bugs."""
230+
231+
def test_post_relative_path_combines_with_api_base(self):
232+
"""Test that POST requests correctly combine relative paths with api_base."""
233+
api_base = "https://api.fireworks.ai"
234+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
235+
236+
mock_response = MagicMock()
237+
mock_response.status_code = 200
238+
239+
with patch.object(client._session, "post", return_value=mock_response) as mock_post:
240+
relative_path = "v1/test/evaluator:getUploadEndpoint"
241+
client.post(relative_path, json={"name": "test"})
242+
243+
mock_post.assert_called_once()
244+
call_args = mock_post.call_args
245+
# Check the URL passed to requests.post
246+
assert call_args[0][0] == f"{api_base}/{relative_path}"
247+
assert not call_args[0][0].startswith(f"{api_base}/{api_base}")
248+
249+
def test_get_relative_path_combines_with_api_base(self):
250+
"""Test that GET requests correctly combine relative paths with api_base."""
251+
api_base = "https://api.fireworks.ai"
252+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
253+
254+
mock_response = MagicMock()
255+
mock_response.status_code = 200
256+
257+
with patch.object(client._session, "get", return_value=mock_response) as mock_get:
258+
relative_path = "verifyApiKey"
259+
client.get(relative_path)
260+
261+
mock_get.assert_called_once()
262+
call_args = mock_get.call_args
263+
assert call_args[0][0] == f"{api_base}/{relative_path}"
264+
265+
def test_post_get_upload_endpoint_path(self):
266+
"""Test the specific getUploadEndpoint path that was buggy.
267+
268+
This ensures relative paths like 'v1/{name}:getUploadEndpoint' are handled correctly
269+
and don't get double-prefixed with api_base.
270+
"""
271+
api_base = "https://api.fireworks.ai"
272+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
273+
274+
mock_response = MagicMock()
275+
mock_response.status_code = 200
276+
mock_response.json.return_value = {"filenameToSignedUrls": {"test.tar.gz": "https://signed.url"}}
277+
278+
with patch.object(client._session, "post", return_value=mock_response) as mock_post:
279+
evaluator_name = "test-evaluator"
280+
# This is the correct pattern - relative path, not full URL
281+
upload_endpoint_path = f"v1/{evaluator_name}:getUploadEndpoint"
282+
client.post(upload_endpoint_path, json={"name": evaluator_name})
283+
284+
mock_post.assert_called_once()
285+
call_args = mock_post.call_args
286+
expected_url = f"{api_base}/{upload_endpoint_path}"
287+
actual_url = call_args[0][0]
288+
assert actual_url == expected_url, f"Expected {expected_url}, got {actual_url}"
289+
# Ensure it doesn't have the buggy double-prefix
290+
assert not actual_url.startswith(f"{api_base}/{api_base}")
291+
292+
def test_post_validate_upload_path(self):
293+
"""Test the specific validateUpload path that was buggy.
294+
295+
This ensures relative paths like 'v1/{name}:validateUpload' are handled correctly.
296+
"""
297+
api_base = "https://api.fireworks.ai"
298+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
299+
300+
mock_response = MagicMock()
301+
mock_response.status_code = 200
302+
mock_response.json.return_value = {"status": "validated"}
303+
304+
with patch.object(client._session, "post", return_value=mock_response) as mock_post:
305+
evaluator_name = "test-evaluator"
306+
# This is the correct pattern - relative path, not full URL
307+
validate_path = f"v1/{evaluator_name}:validateUpload"
308+
client.post(validate_path, json={"name": evaluator_name})
309+
310+
mock_post.assert_called_once()
311+
call_args = mock_post.call_args
312+
expected_url = f"{api_base}/{validate_path}"
313+
actual_url = call_args[0][0]
314+
assert actual_url == expected_url, f"Expected {expected_url}, got {actual_url}"
315+
# Ensure it doesn't have the buggy double-prefix
316+
assert not actual_url.startswith(f"{api_base}/{api_base}")
317+
318+
def test_path_with_leading_slash_stripped(self):
319+
"""Test that leading slashes in paths are correctly handled."""
320+
api_base = "https://api.fireworks.ai"
321+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
322+
323+
mock_response = MagicMock()
324+
mock_response.status_code = 200
325+
326+
with patch.object(client._session, "get", return_value=mock_response) as mock_get:
327+
# Path with leading slash should be handled correctly
328+
client.get("/v1/test/path")
329+
330+
mock_get.assert_called_once()
331+
call_args = mock_get.call_args
332+
# Should not have double slash
333+
assert call_args[0][0] == f"{api_base}/v1/test/path"
334+
335+
def test_api_base_with_trailing_slash(self):
336+
"""Test that api_base with trailing slash is handled correctly."""
337+
api_base = "https://api.fireworks.ai/"
338+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
339+
340+
mock_response = MagicMock()
341+
mock_response.status_code = 200
342+
343+
with patch.object(client._session, "post", return_value=mock_response) as mock_post:
344+
relative_path = "v1/test/path"
345+
client.post(relative_path, json={})
346+
347+
mock_post.assert_called_once()
348+
call_args = mock_post.call_args
349+
# Should not have double slash
350+
assert call_args[0][0] == f"https://api.fireworks.ai/{relative_path}"
351+
352+
def test_all_http_methods_with_relative_paths(self):
353+
"""Test that all HTTP methods correctly handle relative paths."""
354+
api_base = "https://api.fireworks.ai"
355+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
356+
357+
mock_response = MagicMock()
358+
mock_response.status_code = 200
359+
360+
test_path = "v1/accounts/test/evaluators"
361+
362+
methods = [
363+
("get", lambda p: client.get(p)),
364+
("post", lambda p: client.post(p, json={})),
365+
("put", lambda p: client.put(p, json={})),
366+
("patch", lambda p: client.patch(p, json={})),
367+
("delete", lambda p: client.delete(p)),
368+
]
369+
370+
for method_name, method_call in methods:
371+
with patch.object(client._session, method_name, return_value=mock_response) as mock_method:
372+
method_call(test_path)
373+
374+
mock_method.assert_called_once()
375+
call_args = mock_method.call_args
376+
expected_url = f"{api_base}/{test_path}"
377+
actual_url = call_args[0][0]
378+
assert actual_url == expected_url, f"{method_name.upper()} expected {expected_url}, got {actual_url}"
379+
# Ensure no double-prefix bug
380+
assert not actual_url.startswith(f"{api_base}/{api_base}"), (
381+
f"{method_name.upper()} URL has double-prefix bug: {actual_url}"
382+
)
383+
384+
def test_paths_containing_v1_pattern(self):
385+
"""Test various v1 API paths to ensure correct URL construction."""
386+
api_base = "https://api.fireworks.ai"
387+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
388+
389+
mock_response = MagicMock()
390+
mock_response.status_code = 200
391+
392+
test_cases = [
393+
"v1/accounts/test/evaluators",
394+
"v1/accounts/test/evaluators/eval-id",
395+
"v1/accounts/test/evaluatorsV2",
396+
"v1/accounts/test/evaluators:previewEvaluator",
397+
"v1/test-evaluator:getUploadEndpoint",
398+
"v1/test-evaluator:validateUpload",
399+
]
400+
401+
with patch.object(client._session, "post", return_value=mock_response) as mock_post:
402+
for path in test_cases:
403+
client.post(path, json={})
404+
405+
call_args = mock_post.call_args
406+
actual_url = call_args[0][0]
407+
expected_url = f"{api_base}/{path}"
408+
409+
assert actual_url == expected_url, (
410+
f"Path '{path}' resulted in URL '{actual_url}', expected '{expected_url}'"
411+
)
412+
assert not actual_url.startswith(f"{api_base}/{api_base}"), (
413+
f"Path '{path}' has double-prefix bug: {actual_url}"
414+
)
415+
416+
mock_post.reset_mock()
417+
418+
def test_full_url_passed_by_mistake_detected(self):
419+
"""Test that accidentally passing a full URL instead of relative path is detected.
420+
421+
This test documents the bug pattern: if a full URL like '{api_base}/v1/path'
422+
is passed instead of a relative path like 'v1/path', it will result in a
423+
malformed URL like '{api_base}/{api_base}/v1/path'.
424+
425+
This test verifies that our code correctly handles relative paths (which prevents
426+
the bug), and documents what would happen if the bug occurred.
427+
"""
428+
api_base = "https://api.fireworks.ai"
429+
client = FireworksAPIClient(api_key="test_key", api_base=api_base)
430+
431+
mock_response = MagicMock()
432+
mock_response.status_code = 200
433+
434+
# CORRECT: Relative path (what we should use)
435+
with patch.object(client._session, "post", return_value=mock_response) as mock_post:
436+
correct_relative_path = "v1/test-evaluator:getUploadEndpoint"
437+
client.post(correct_relative_path, json={})
438+
439+
call_args = mock_post.call_args
440+
correct_url = call_args[0][0]
441+
expected_correct_url = f"{api_base}/{correct_relative_path}"
442+
assert correct_url == expected_correct_url
443+
444+
# INCORRECT: Full URL (this would cause the bug - but we're not actually testing this,
445+
# just documenting that our current implementation would create a malformed URL)
446+
# If someone accidentally did: client.post(f"{api_base}/v1/path", ...)
447+
# The result would be: f"{api_base}/{api_base}/v1/path" which is wrong.
448+
# Our tests above ensure we use relative paths, preventing this bug.
449+
mock_post.reset_mock()
450+
with patch.object(client._session, "post", return_value=mock_response) as mock_post:
451+
# Simulating what WOULD happen if buggy code passed full URL
452+
buggy_full_url = f"{api_base}/v1/test-evaluator:getUploadEndpoint"
453+
client.post(buggy_full_url, json={})
454+
455+
call_args = mock_post.call_args
456+
buggy_url = call_args[0][0]
457+
# This shows what the buggy URL would look like
458+
buggy_expected = f"{api_base}/{buggy_full_url}"
459+
460+
# This assertion documents the bug pattern - the URL would be malformed
461+
assert buggy_url == buggy_expected
462+
assert buggy_url.startswith(f"{api_base}/{api_base}"), (
463+
"This documents the bug: passing full URL creates double-prefix. Always use relative paths!"
464+
)
465+
466+
228467
if __name__ == "__main__":
229468
pytest.main([__file__, "-v"])

0 commit comments

Comments
 (0)