apigateway / tests /test_response_inspector.py
jebin2's picture
Add response inspector tests
0fe9140
"""
Test suite for Response Inspector
Tests response analysis logic for determining credit actions:
- Confirm credits (successful operations)
- Refund credits (failed operations)
- Keep reserved (pending async operations)
"""
import pytest
import json
from fastapi import Response
from services.credit_service.response_inspector import ResponseInspector
# =============================================================================
# Synchronous Endpoint Tests
# =============================================================================
def test_should_confirm_sync_success():
"""Test confirmation for successful sync operation (200)."""
response = Response(content=json.dumps({"result": "success"}), status_code=200)
inspector = ResponseInspector()
assert inspector.should_confirm(response, "sync", {"result": "success"}) is True
assert inspector.should_refund(response, "sync", {"result": "success"}) is False
def test_should_confirm_sync_created():
"""Test confirmation for sync operation (201)."""
response = Response(content=json.dumps({"id": "123"}), status_code=201)
inspector = ResponseInspector()
assert inspector.should_confirm(response, "sync", {"id": "123"}) is True
def test_should_refund_sync_client_error():
"""Test refund for sync client error (400)."""
response = Response(
content=json.dumps({"detail": "Invalid request"}),
status_code=400
)
inspector = ResponseInspector()
assert inspector.should_confirm(response, "sync", {"detail": "Invalid request"}) is False
assert inspector.should_refund(response, "sync", {"detail": "Invalid request"}) is True
def test_should_refund_sync_server_error():
"""Test refund for sync server error (500)."""
response = Response(
content=json.dumps({"detail": "Internal error"}),
status_code=500
)
inspector = ResponseInspector()
assert inspector.should_refund(response, "sync", {"detail": "Internal error"}) is True
# =============================================================================
# Asynchronous Endpoint Tests - Job Creation
# =============================================================================
def test_async_job_creation_success():
"""Test async job creation - should keep reserved."""
response = Response(
content=json.dumps({"job_id": "job_123", "status": "queued"}),
status_code=200
)
inspector = ResponseInspector()
response_data = {"job_id": "job_123", "status": "queued"}
# Job created successfully, but not complete yet
assert inspector.should_confirm(response, "async", response_data) is False
assert inspector.should_refund(response, "async", response_data) is False
def test_async_job_creation_failure():
"""Test async job creation failure - should refund."""
response = Response(
content=json.dumps({"detail": "Validation failed"}),
status_code=400
)
inspector = ResponseInspector()
response_data = {"detail": "Validation failed"}
# Job creation failed, refund credits
assert inspector.should_confirm(response, "async", response_data) is False
assert inspector.should_refund(response, "async", response_data) is True
# =============================================================================
# Asynchronous Endpoint Tests - Status Checks
# =============================================================================
def test_async_status_completed():
"""Test async job status check - completed."""
response = Response(
content=json.dumps({"job_id": "job_123", "status": "completed", "result": "..."}),
status_code=200
)
inspector = ResponseInspector()
response_data = {"job_id": "job_123", "status": "completed", "result": "..."}
# Job completed, confirm credits
assert inspector.should_confirm(response, "async", response_data) is True
assert inspector.should_refund(response, "async", response_data) is False
def test_async_status_processing():
"""Test async job status check - still processing."""
response = Response(
content=json.dumps({"job_id": "job_123", "status": "processing"}),
status_code=200
)
inspector = ResponseInspector()
response_data = {"job_id": "job_123", "status": "processing"}
# Job still processing, keep reserved
assert inspector.should_confirm(response, "async", response_data) is False
assert inspector.should_refund(response, "async", response_data) is False
def test_async_status_queued():
"""Test async job status check - still queued."""
response = Response(
content=json.dumps({"job_id": "job_123", "status": "queued", "position": 5}),
status_code=200
)
inspector = ResponseInspector()
response_data = {"job_id": "job_123", "status": "queued", "position": 5}
# Job still queued, keep reserved
assert inspector.should_confirm(response, "async", response_data) is False
assert inspector.should_refund(response, "async", response_data) is False
def test_async_status_failed_refundable():
"""Test async job status check - failed with refundable error."""
response = Response(
content=json.dumps({
"job_id": "job_123",
"status": "failed",
"error_message": "API_KEY_INVALID - The API key is invalid"
}),
status_code=200
)
inspector = ResponseInspector()
response_data = {
"job_id": "job_123",
"status": "failed",
"error_message": "API_KEY_INVALID - The API key is invalid"
}
# Job failed with refundable error, refund credits
assert inspector.should_confirm(response, "async", response_data) is False
assert inspector.should_refund(response, "async", response_data) is True
def test_async_status_failed_non_refundable():
"""Test async job status check - failed with non-refundable error."""
response = Response(
content=json.dumps({
"job_id": "job_123",
"status": "failed",
"error_message": "SAFETY: Content blocked by safety filters"
}),
status_code=200
)
inspector = ResponseInspector()
response_data = {
"job_id": "job_123",
"status": "failed",
"error_message": "SAFETY: Content blocked by safety filters"
}
# Job failed with non-refundable error, confirm credits (keep deducted)
assert inspector.should_confirm(response, "async", response_data) is False
assert inspector.should_refund(response, "async", response_data) is False
# =============================================================================
# Refund Reason Tests
# =============================================================================
def test_get_refund_reason_server_error():
"""Test refund reason for server error."""
response = Response(content="", status_code=500)
inspector = ResponseInspector()
reason = inspector.get_refund_reason(response, None)
assert "Server error: 500" == reason
def test_get_refund_reason_client_error():
"""Test refund reason for client error."""
response = Response(
content=json.dumps({"detail": "Invalid input"}),
status_code=400
)
inspector = ResponseInspector()
response_data = {"detail": "Invalid input"}
reason = inspector.get_refund_reason(response, response_data)
assert "Request error: Invalid input" == reason
def test_get_refund_reason_job_failure():
"""Test refund reason for job failure."""
response = Response(content="", status_code=200)
inspector = ResponseInspector()
response_data = {"error_message": "API timeout after 60 seconds"}
reason = inspector.get_refund_reason(response, response_data)
assert "Job failed" in reason
assert "API timeout" in reason
def test_get_refund_reason_unknown():
"""Test refund reason when unknown."""
response = Response(content="", status_code=200)
inspector = ResponseInspector()
reason = inspector.get_refund_reason(response, None)
assert reason == "Unknown error"
# =============================================================================
# Response Body Parsing Tests
# =============================================================================
def test_parse_response_body_valid_json():
"""Test parsing valid JSON response body."""
body = b'{"key": "value", "number": 123}'
inspector = ResponseInspector()
parsed = inspector.parse_response_body(body)
assert parsed == {"key": "value", "number": 123}
def test_parse_response_body_invalid_json():
"""Test parsing invalid JSON response body."""
body = b'not json'
inspector = ResponseInspector()
parsed = inspector.parse_response_body(body)
assert parsed is None
def test_parse_response_body_empty():
"""Test parsing empty response body."""
body = b''
inspector = ResponseInspector()
parsed = inspector.parse_response_body(body)
assert parsed is None
# =============================================================================
# Edge Cases
# =============================================================================
def test_free_endpoint():
"""Test free endpoint (no credit cost)."""
response = Response(content=json.dumps({"result": "ok"}), status_code=200)
inspector = ResponseInspector()
# Free endpoints shouldn't trigger credit actions
# (handled by middleware checking cost=0, but inspector should be safe)
assert inspector.should_confirm(response, "free", {"result": "ok"}) is False
def test_async_missing_status_field():
"""Test async response missing status field."""
response = Response(
content=json.dumps({"job_id": "abc", "message": "Processing"}),
status_code=200
)
inspector = ResponseInspector()
response_data = {"job_id": "abc", "message": "Processing"}
# No status field, should keep reserved
assert inspector.should_confirm(response, "async", response_data) is False
assert inspector.should_refund(response, "async", response_data) is False
def test_async_none_response_data():
"""Test async with None response data."""
response = Response(content=b"", status_code=500)
inspector = ResponseInspector()
# Server error with no parseable response
assert inspector.should_confirm(response, "async", None) is False
assert inspector.should_refund(response, "async", None) is True