Skip to content

Commit 568ffbf

Browse files
cx-lukas-salkauskas-xLukas Šalkauskas
authored andcommitted
fix error handling
1 parent 9362f08 commit 568ffbf

File tree

4 files changed

+137
-7
lines changed

4 files changed

+137
-7
lines changed

inapppy/appstore.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,23 @@ def _prepare_receipt(self, receipt: str, shared_secret: str, exclude_old_transac
9393
def post_json(self, request_json: dict) -> dict:
9494
self._change_url_by_sandbox()
9595

96+
response = None
9697
try:
97-
return requests.post(self.url, json=request_json, timeout=self.http_timeout).json()
98-
except (ValueError, RequestException):
99-
raise InAppPyValidationError("HTTP error")
98+
response = requests.post(self.url, json=request_json, timeout=self.http_timeout)
99+
return response.json()
100+
except (ValueError, RequestException) as e:
101+
# Build raw_response with available information
102+
raw_response = {"error": str(e)}
103+
104+
# Try to include response details if available
105+
if response is not None:
106+
raw_response["status_code"] = response.status_code
107+
try:
108+
raw_response["content"] = response.text
109+
except Exception:
110+
pass
111+
112+
raise InAppPyValidationError("HTTP error", raw_response=raw_response)
100113

101114
@staticmethod
102115
def _ms_timestamp_expired(ms_timestamp: str) -> bool:

inapppy/asyncio/appstore.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,29 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
2727

2828
async def post_json(self, request_json: dict) -> dict:
2929
self._change_url_by_sandbox()
30+
response_text = None
31+
status_code = None
3032
try:
3133
async with self._session.post(
3234
self.url, json=request_json, timeout=ClientTimeout(total=self.http_timeout)
3335
) as resp:
34-
return await resp.json(content_type=None)
35-
except (ValueError, ClientError):
36-
raise InAppPyValidationError("HTTP error")
36+
status_code = resp.status
37+
response_text = await resp.text()
38+
# Try to parse as JSON
39+
import json
40+
41+
return json.loads(response_text)
42+
except (ValueError, ClientError) as e:
43+
# Build raw_response with available information
44+
raw_response = {"error": str(e)}
45+
46+
# Try to include response details if available
47+
if status_code is not None:
48+
raw_response["status_code"] = status_code
49+
if response_text is not None:
50+
raw_response["content"] = response_text
51+
52+
raise InAppPyValidationError("HTTP error", raw_response=raw_response)
3753

3854
async def validate(self, receipt: str, shared_secret: str = None, exclude_old_transactions: bool = False) -> dict:
3955
"""Validates receipt against apple services.

tests/asyncio/test_asyncio_appstore.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from unittest.mock import patch
1+
from unittest.mock import Mock, patch
22

33
import pytest
44

@@ -119,3 +119,67 @@ async def test_appstore_async_context_manager():
119119

120120
# Verify session is closed after exiting context
121121
assert validator._session is None
122+
123+
124+
@pytest.mark.asyncio
125+
async def test_appstore_async_http_error_includes_raw_response(appstore_validator: AppStoreValidator):
126+
"""Test that async HTTP errors include raw_response with status code and content"""
127+
from unittest.mock import AsyncMock, Mock
128+
129+
# Create a mock response
130+
mock_resp = Mock()
131+
mock_resp.status = 503
132+
mock_resp.text = AsyncMock(return_value="Service Unavailable")
133+
134+
# Create a mock session post that returns our mock response
135+
def mock_post(*args, **kwargs):
136+
class MockContext:
137+
async def __aenter__(self):
138+
return mock_resp
139+
140+
async def __aexit__(self, *args):
141+
pass
142+
143+
return MockContext()
144+
145+
with pytest.raises(InAppPyValidationError) as exc_info:
146+
appstore_validator._session = Mock()
147+
appstore_validator._session.post = mock_post
148+
# Mock json.loads to raise ValueError (simulating invalid JSON)
149+
with patch("json.loads", side_effect=ValueError("Invalid JSON")):
150+
await appstore_validator.post_json({"receipt-data": "test"})
151+
152+
# Verify raw_response is not None and contains useful information
153+
assert exc_info.value.raw_response is not None
154+
assert "error" in exc_info.value.raw_response
155+
assert "status_code" in exc_info.value.raw_response
156+
assert exc_info.value.raw_response["status_code"] == 503
157+
assert "content" in exc_info.value.raw_response
158+
assert exc_info.value.raw_response["content"] == "Service Unavailable"
159+
160+
161+
@pytest.mark.asyncio
162+
async def test_appstore_async_network_error_includes_raw_response(appstore_validator: AppStoreValidator):
163+
"""Test that async network errors include raw_response with error details"""
164+
from aiohttp import ClientError
165+
166+
# Create a mock session that raises ClientError
167+
def mock_post(*args, **kwargs):
168+
class MockContext:
169+
async def __aenter__(self):
170+
raise ClientError("Connection timeout")
171+
172+
async def __aexit__(self, *args):
173+
pass
174+
175+
return MockContext()
176+
177+
with pytest.raises(InAppPyValidationError) as exc_info:
178+
appstore_validator._session = Mock()
179+
appstore_validator._session.post = mock_post
180+
await appstore_validator.post_json({"receipt-data": "test"})
181+
182+
# Verify raw_response is not None and contains error information
183+
assert exc_info.value.raw_response is not None
184+
assert "error" in exc_info.value.raw_response
185+
assert "Connection timeout" in exc_info.value.raw_response["error"]

tests/test_appstore.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,40 @@ def test_appstore_auto_retry_wrong_env_request(appstore_validator_auto_retry_on_
9191
assert mock_method.call_count == 1
9292
assert validator.url == "https://buy.itunes.apple.com/verifyReceipt"
9393
assert mock_method.call_args[0][0] == {"receipt-data": "test-receipt", "password": "shared-secret"}
94+
95+
96+
def test_appstore_http_error_includes_raw_response(appstore_validator: AppStoreValidator):
97+
"""Test that HTTP errors include raw_response with status code and content"""
98+
from unittest.mock import Mock
99+
100+
# Mock a response that has status code but invalid JSON
101+
mock_response = Mock()
102+
mock_response.status_code = 503
103+
mock_response.text = "Service Unavailable"
104+
mock_response.json.side_effect = ValueError("No JSON object could be decoded")
105+
106+
with pytest.raises(InAppPyValidationError) as exc_info:
107+
with patch("requests.post", return_value=mock_response):
108+
appstore_validator.post_json({"receipt-data": "test"})
109+
110+
# Verify raw_response is not None and contains useful information
111+
assert exc_info.value.raw_response is not None
112+
assert "error" in exc_info.value.raw_response
113+
assert "status_code" in exc_info.value.raw_response
114+
assert exc_info.value.raw_response["status_code"] == 503
115+
assert "content" in exc_info.value.raw_response
116+
assert exc_info.value.raw_response["content"] == "Service Unavailable"
117+
118+
119+
def test_appstore_network_error_includes_raw_response(appstore_validator: AppStoreValidator):
120+
"""Test that network errors include raw_response with error details"""
121+
from requests.exceptions import RequestException
122+
123+
with pytest.raises(InAppPyValidationError) as exc_info:
124+
with patch("requests.post", side_effect=RequestException("Connection timeout")):
125+
appstore_validator.post_json({"receipt-data": "test"})
126+
127+
# Verify raw_response is not None and contains error information
128+
assert exc_info.value.raw_response is not None
129+
assert "error" in exc_info.value.raw_response
130+
assert "Connection timeout" in exc_info.value.raw_response["error"]

0 commit comments

Comments
 (0)