Files
urbanLifeline/dify/api/tests/unit_tests/services/test_billing_service.py
2025-12-01 17:21:38 +08:00

237 lines
9.9 KiB
Python

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