dify
This commit is contained in:
236
dify/api/tests/unit_tests/services/test_billing_service.py
Normal file
236
dify/api/tests/unit_tests/services/test_billing_service.py
Normal file
@@ -0,0 +1,236 @@
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
|
||||
from services.billing_service import BillingService
|
||||
|
||||
|
||||
class TestBillingServiceSendRequest:
|
||||
"""Unit tests for BillingService._send_request method."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_httpx_request(self):
|
||||
"""Mock httpx.request for testing."""
|
||||
with patch("services.billing_service.httpx.request") as mock_request:
|
||||
yield mock_request
|
||||
|
||||
@pytest.fixture
|
||||
def mock_billing_config(self):
|
||||
"""Mock BillingService configuration."""
|
||||
with (
|
||||
patch.object(BillingService, "base_url", "https://billing-api.example.com"),
|
||||
patch.object(BillingService, "secret_key", "test-secret-key"),
|
||||
):
|
||||
yield
|
||||
|
||||
def test_get_request_success(self, mock_httpx_request, mock_billing_config):
|
||||
"""Test successful GET request."""
|
||||
# Arrange
|
||||
expected_response = {"result": "success", "data": {"info": "test"}}
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = httpx.codes.OK
|
||||
mock_response.json.return_value = expected_response
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act
|
||||
result = BillingService._send_request("GET", "/test", params={"key": "value"})
|
||||
|
||||
# Assert
|
||||
assert result == expected_response
|
||||
mock_httpx_request.assert_called_once()
|
||||
call_args = mock_httpx_request.call_args
|
||||
assert call_args[0][0] == "GET"
|
||||
assert call_args[0][1] == "https://billing-api.example.com/test"
|
||||
assert call_args[1]["params"] == {"key": "value"}
|
||||
assert call_args[1]["headers"]["Billing-Api-Secret-Key"] == "test-secret-key"
|
||||
assert call_args[1]["headers"]["Content-Type"] == "application/json"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code", [httpx.codes.NOT_FOUND, httpx.codes.INTERNAL_SERVER_ERROR, httpx.codes.BAD_REQUEST]
|
||||
)
|
||||
def test_get_request_non_200_status_code(self, mock_httpx_request, mock_billing_config, status_code):
|
||||
"""Test GET request with non-200 status code raises ValueError."""
|
||||
# Arrange
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = status_code
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
BillingService._send_request("GET", "/test")
|
||||
assert "Unable to retrieve billing information" in str(exc_info.value)
|
||||
|
||||
def test_put_request_success(self, mock_httpx_request, mock_billing_config):
|
||||
"""Test successful PUT request."""
|
||||
# Arrange
|
||||
expected_response = {"result": "success"}
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = httpx.codes.OK
|
||||
mock_response.json.return_value = expected_response
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act
|
||||
result = BillingService._send_request("PUT", "/test", json={"key": "value"})
|
||||
|
||||
# Assert
|
||||
assert result == expected_response
|
||||
call_args = mock_httpx_request.call_args
|
||||
assert call_args[0][0] == "PUT"
|
||||
|
||||
def test_put_request_internal_server_error(self, mock_httpx_request, mock_billing_config):
|
||||
"""Test PUT request with INTERNAL_SERVER_ERROR raises InternalServerError."""
|
||||
# Arrange
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = httpx.codes.INTERNAL_SERVER_ERROR
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(InternalServerError) as exc_info:
|
||||
BillingService._send_request("PUT", "/test", json={"key": "value"})
|
||||
assert exc_info.value.code == 500
|
||||
assert "Unable to process billing request" in str(exc_info.value.description)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code", [httpx.codes.BAD_REQUEST, httpx.codes.NOT_FOUND, httpx.codes.UNAUTHORIZED, httpx.codes.FORBIDDEN]
|
||||
)
|
||||
def test_put_request_non_200_non_500(self, mock_httpx_request, mock_billing_config, status_code):
|
||||
"""Test PUT request with non-200 and non-500 status code raises ValueError."""
|
||||
# Arrange
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = status_code
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
BillingService._send_request("PUT", "/test", json={"key": "value"})
|
||||
assert "Invalid arguments." in str(exc_info.value)
|
||||
|
||||
@pytest.mark.parametrize("method", ["POST", "DELETE"])
|
||||
def test_non_get_non_put_request_success(self, mock_httpx_request, mock_billing_config, method):
|
||||
"""Test successful POST/DELETE request."""
|
||||
# Arrange
|
||||
expected_response = {"result": "success"}
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = httpx.codes.OK
|
||||
mock_response.json.return_value = expected_response
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act
|
||||
result = BillingService._send_request(method, "/test", json={"key": "value"})
|
||||
|
||||
# Assert
|
||||
assert result == expected_response
|
||||
call_args = mock_httpx_request.call_args
|
||||
assert call_args[0][0] == method
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code", [httpx.codes.BAD_REQUEST, httpx.codes.INTERNAL_SERVER_ERROR, httpx.codes.NOT_FOUND]
|
||||
)
|
||||
def test_post_request_non_200_with_valid_json(self, mock_httpx_request, mock_billing_config, status_code):
|
||||
"""Test POST request with non-200 status code raises ValueError."""
|
||||
# Arrange
|
||||
error_response = {"detail": "Error message"}
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = status_code
|
||||
mock_response.json.return_value = error_response
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
BillingService._send_request("POST", "/test", json={"key": "value"})
|
||||
assert "Unable to send request to" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code", [httpx.codes.BAD_REQUEST, httpx.codes.INTERNAL_SERVER_ERROR, httpx.codes.NOT_FOUND]
|
||||
)
|
||||
def test_delete_request_non_200_with_valid_json(self, mock_httpx_request, mock_billing_config, status_code):
|
||||
"""Test DELETE request with non-200 status code but valid JSON response.
|
||||
|
||||
DELETE doesn't check status code, so it returns the error JSON.
|
||||
"""
|
||||
# Arrange
|
||||
error_response = {"detail": "Error message"}
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = status_code
|
||||
mock_response.json.return_value = error_response
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act
|
||||
result = BillingService._send_request("DELETE", "/test", json={"key": "value"})
|
||||
|
||||
# Assert
|
||||
assert result == error_response
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code", [httpx.codes.BAD_REQUEST, httpx.codes.INTERNAL_SERVER_ERROR, httpx.codes.NOT_FOUND]
|
||||
)
|
||||
def test_post_request_non_200_with_invalid_json(self, mock_httpx_request, mock_billing_config, status_code):
|
||||
"""Test POST request with non-200 status code raises ValueError before JSON parsing."""
|
||||
# Arrange
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = status_code
|
||||
mock_response.text = ""
|
||||
mock_response.json.side_effect = json.JSONDecodeError("Expecting value", "", 0)
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act & Assert
|
||||
# POST checks status code before calling response.json(), so ValueError is raised
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
BillingService._send_request("POST", "/test", json={"key": "value"})
|
||||
assert "Unable to send request to" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status_code", [httpx.codes.BAD_REQUEST, httpx.codes.INTERNAL_SERVER_ERROR, httpx.codes.NOT_FOUND]
|
||||
)
|
||||
def test_delete_request_non_200_with_invalid_json(self, mock_httpx_request, mock_billing_config, status_code):
|
||||
"""Test DELETE request with non-200 status code and invalid JSON response raises exception.
|
||||
|
||||
DELETE doesn't check status code, so it calls response.json() which raises JSONDecodeError
|
||||
when the response cannot be parsed as JSON (e.g., empty response).
|
||||
"""
|
||||
# Arrange
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = status_code
|
||||
mock_response.text = ""
|
||||
mock_response.json.side_effect = json.JSONDecodeError("Expecting value", "", 0)
|
||||
mock_httpx_request.return_value = mock_response
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
BillingService._send_request("DELETE", "/test", json={"key": "value"})
|
||||
|
||||
def test_retry_on_request_error(self, mock_httpx_request, mock_billing_config):
|
||||
"""Test that _send_request retries on httpx.RequestError."""
|
||||
# Arrange
|
||||
expected_response = {"result": "success"}
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = httpx.codes.OK
|
||||
mock_response.json.return_value = expected_response
|
||||
|
||||
# First call raises RequestError, second succeeds
|
||||
mock_httpx_request.side_effect = [
|
||||
httpx.RequestError("Network error"),
|
||||
mock_response,
|
||||
]
|
||||
|
||||
# Act
|
||||
result = BillingService._send_request("GET", "/test")
|
||||
|
||||
# Assert
|
||||
assert result == expected_response
|
||||
assert mock_httpx_request.call_count == 2
|
||||
|
||||
def test_retry_exhausted_raises_exception(self, mock_httpx_request, mock_billing_config):
|
||||
"""Test that _send_request raises exception after retries are exhausted."""
|
||||
# Arrange
|
||||
mock_httpx_request.side_effect = httpx.RequestError("Network error")
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(httpx.RequestError):
|
||||
BillingService._send_request("GET", "/test")
|
||||
|
||||
# Should retry multiple times (wait=2, stop_before_delay=10 means ~5 attempts)
|
||||
assert mock_httpx_request.call_count > 1
|
||||
Reference in New Issue
Block a user