@@ -409,6 +409,54 @@ def test_handle_error_response_with_empty_error_data(base_resource, mock_logger)
409409# -----------------------------------------------------------------------------
410410
411411
412+ def test_get_retry_after_with_valid_header (base_resource ):
413+ """Test that _get_retry_after correctly parses a valid Retry-After header with a digit."""
414+ # Line 337: response.headers.get("Retry-After")
415+ mock_response = Mock ()
416+ mock_response .headers = {"Retry-After" : "30" }
417+
418+ # Set up retry parameters
419+ base_resource .retry_after_seconds = 10
420+ base_resource .retry_backoff_factor = 2
421+
422+ retry_seconds = base_resource ._get_retry_after (mock_response , 1 )
423+
424+ # Should use the header value (30) instead of calculated backoff
425+ assert retry_seconds == 30
426+
427+
428+ def test_get_retry_after_with_invalid_header (base_resource ):
429+ """Test that _get_retry_after falls back to calculated backoff when Retry-After header is not a digit."""
430+ mock_response = Mock ()
431+ mock_response .headers = {"Retry-After" : "not-a-number" }
432+
433+ # Set up retry parameters
434+ base_resource .retry_after_seconds = 10
435+ base_resource .retry_backoff_factor = 2
436+
437+ # For retry_count=1, should be 10 * (2^1) = 20
438+ retry_seconds = base_resource ._get_retry_after (mock_response , 1 )
439+
440+ # Should use calculated backoff
441+ assert retry_seconds == 20
442+
443+
444+ def test_get_retry_after_without_header (base_resource ):
445+ """Test that _get_retry_after falls back to calculated backoff when Retry-After header is missing."""
446+ mock_response = Mock ()
447+ mock_response .headers = {} # No Retry-After header
448+
449+ # Set up retry parameters
450+ base_resource .retry_after_seconds = 10
451+ base_resource .retry_backoff_factor = 2
452+
453+ # For retry_count=0, should be 10 * (2^0) = 10
454+ retry_seconds = base_resource ._get_retry_after (mock_response , 0 )
455+
456+ # Should use calculated backoff
457+ assert retry_seconds == 10
458+
459+
412460@patch ("fitbit_client.resources.base.sleep" )
413461def test_rate_limit_retries (
414462 mock_sleep , base_resource , mock_oauth_session , mock_response_factory , mock_logger
@@ -529,7 +577,146 @@ def test_rate_limit_max_retries_exhausted(
529577
530578
531579# -----------------------------------------------------------------------------
532- # 11. API Error Status Codes
580+ # 11. Direct Request Testing
581+ # -----------------------------------------------------------------------------
582+
583+
584+ @patch ("builtins.print" )
585+ @patch ("fitbit_client.resources.base.CurlDebugMixin._build_curl_command" )
586+ def test_make_direct_request_with_debug (mock_build_curl , mock_print , base_resource ):
587+ """Test that _make_direct_request returns empty dict when debug=True."""
588+ # Mock the _build_curl_command method
589+ mock_build_curl .return_value = "curl -X GET https://example.com"
590+
591+ # The actual method signature is different from what we tried to test
592+ result = base_resource ._make_direct_request ("/test" , debug = True )
593+
594+ # Should return empty dict in debug mode
595+ assert result == {}
596+
597+ # Should print the curl command
598+ mock_print .assert_called ()
599+
600+
601+ @patch ("fitbit_client.resources.base.BaseResource._handle_json_response" )
602+ def test_make_direct_request_success (mock_handle_json , base_resource ):
603+ """Test successful direct request with JSON response."""
604+ # Mock the OAuth session
605+ base_resource .oauth = Mock ()
606+
607+ # Create a mock response
608+ mock_response = Mock ()
609+ mock_response .status_code = 200
610+ mock_response .headers = {"content-type" : "application/json" }
611+ base_resource .oauth .request .return_value = mock_response
612+
613+ # Mock the _handle_json_response method
614+ mock_handle_json .return_value = {"data" : "test" }
615+
616+ # Call the method
617+ result = base_resource ._make_direct_request ("/test" )
618+
619+ # Should return the JSON data
620+ assert result == {"data" : "test" }
621+
622+ # Verify the request was made
623+ base_resource .oauth .request .assert_called_once ()
624+ mock_handle_json .assert_called_once ()
625+
626+
627+ @patch ("fitbit_client.resources.base.BaseResource._get_calling_method" )
628+ def test_make_direct_request_unexpected_content_type (mock_get_calling , base_resource , mock_logger ):
629+ """Test handling of unexpected content type in direct request."""
630+ mock_get_calling .return_value = "test_method"
631+
632+ # Mock the OAuth session
633+ base_resource .oauth = Mock ()
634+
635+ # Create a mock response
636+ mock_response = Mock ()
637+ mock_response .status_code = 200
638+ mock_response .headers = {"content-type" : "text/plain" }
639+ base_resource .oauth .request .return_value = mock_response
640+
641+ # Call the method
642+ result = base_resource ._make_direct_request ("/test" )
643+
644+ # Should return empty dict for unexpected content type
645+ assert result == {}
646+
647+ # Should log an error about unexpected content type
648+ mock_logger .error .assert_called_once ()
649+ assert "Unexpected content type" in mock_logger .error .call_args [0 ][0 ]
650+
651+
652+ @patch ("fitbit_client.resources.base.sleep" )
653+ @patch ("fitbit_client.resources.base.BaseResource._handle_error_response" )
654+ @patch ("fitbit_client.resources.base.BaseResource._should_retry_request" )
655+ def test_make_direct_request_rate_limit_retry (
656+ mock_should_retry , mock_handle_error , mock_sleep , base_resource , mock_logger
657+ ):
658+ """Test retry behavior for rate-limited requests."""
659+ # Configure the resource with custom retry settings
660+ base_resource .max_retries = 1
661+ base_resource .retry_after_seconds = 10
662+ base_resource .retry_backoff_factor = 1
663+
664+ # Mock the OAuth session
665+ base_resource .oauth = Mock ()
666+
667+ # Create a mock response for error and success
668+ error_response = Mock ()
669+ error_response .status_code = 429
670+ error_response .headers = {"Retry-After" : "5" }
671+
672+ success_response = Mock ()
673+ success_response .status_code = 200
674+ success_response .headers = {"content-type" : "application/json" }
675+ success_response .json .return_value = {"data" : "success" }
676+
677+ # Set up the mock to return error first, then success
678+ base_resource .oauth .request .side_effect = [error_response , success_response ]
679+
680+ # Set up mocks for retry logic
681+ mock_handle_error .side_effect = RateLimitExceededException (
682+ message = "Too many requests" , status_code = 429 , error_type = "rate_limit_exceeded"
683+ )
684+ mock_should_retry .return_value = True
685+
686+ # Call the method
687+ with patch (
688+ "fitbit_client.resources.base.BaseResource._handle_json_response"
689+ ) as mock_handle_json :
690+ mock_handle_json .return_value = {"data" : "success" }
691+ result = base_resource ._make_direct_request ("/test" )
692+
693+ # Verify results
694+ assert result == {"data" : "success" }
695+ assert base_resource .oauth .request .call_count == 2
696+ assert mock_sleep .call_count == 1
697+ assert mock_logger .warning .call_count == 1
698+
699+
700+ @patch ("fitbit_client.resources.base.BaseResource._get_calling_method" )
701+ def test_make_direct_request_exception (mock_get_calling , base_resource , mock_logger ):
702+ """Test handling of exceptions in direct request."""
703+ mock_get_calling .return_value = "test_method"
704+
705+ # Mock the OAuth session
706+ base_resource .oauth = Mock ()
707+ base_resource .oauth .request .side_effect = ConnectionError ("Network error" )
708+
709+ # Call the method
710+ with raises (Exception ) as exc_info :
711+ base_resource ._make_direct_request ("/test" )
712+
713+ # Verify exception and logging
714+ assert "Pagination request failed" in str (exc_info .value )
715+ assert mock_logger .error .call_count == 1
716+
717+
718+ # -----------------------------------------------------------------------------
719+ # 12. API Error Status Codes
533720# -----------------------------------------------------------------------------
534721
535722
0 commit comments