@@ -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+
228467if __name__ == "__main__" :
229468 pytest .main ([__file__ , "-v" ])
0 commit comments