Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 194 additions & 0 deletions tests/unit/base/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,60 @@ def test_delete_not_found(self):

self.assertIn("Unable to delete record", str(context.exception))

def test_delete_with_response_body(self):
"""Test delete that returns JSON response body (V1 API style)"""
self.holodeck.mock(
Response(
202,
'{"sid": "DE123", "status": "deleted", "account_sid": "AC123"}',
),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Resources/DE123.json",
),
)
result = self.client.api.v2010.delete(
method="DELETE", uri="/Accounts/AC123/Resources/DE123.json"
)

self.assertIsInstance(result, dict)
self.assertEqual(result["sid"], "DE123")
self.assertEqual(result["status"], "deleted")
self.assertEqual(result["account_sid"], "AC123")

def test_delete_no_content(self):
"""Test traditional delete with no content (204)"""
self.holodeck.mock(
Response(204, ""),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Messages/MM123.json",
),
)
result = self.client.api.v2010.delete(
method="DELETE", uri="/Accounts/AC123/Messages/MM123.json"
)

self.assertTrue(result)
self.assertIsInstance(result, bool)

def test_delete_with_invalid_json(self):
"""Test delete with response content that is not valid JSON"""
self.holodeck.mock(
Response(200, "Not valid JSON content"),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Resources/DE123.json",
),
)
result = self.client.api.v2010.delete(
method="DELETE", uri="/Accounts/AC123/Resources/DE123.json"
)

# Should fallback to boolean True when JSON parsing fails
self.assertTrue(result)
self.assertIsInstance(result, bool)


class VersionExceptionTestCase(unittest.TestCase):
"""Test cases for base Version.exception() method with RFC-9457 auto-detection"""
Expand Down Expand Up @@ -718,6 +772,73 @@ def test_delete_with_response_info_error(self):

self.assertIn("Unable to delete record", str(context.exception))

def test_delete_with_response_info_json_body(self):
"""Test delete_with_response_info with JSON response body"""
self.holodeck.mock(
Response(
202,
'{"sid": "DE123", "status": "deleted", "account_sid": "AC123"}',
{"X-Delete-Header": "deleted", "X-Request-Id": "req123"},
),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Resources/DE123.json",
),
)
result, status_code, headers = self.client.api.v2010.delete_with_response_info(
method="DELETE", uri="/Accounts/AC123/Resources/DE123.json"
)

self.assertIsInstance(result, dict)
self.assertEqual(result["sid"], "DE123")
self.assertEqual(result["status"], "deleted")
self.assertEqual(result["account_sid"], "AC123")
self.assertEqual(status_code, 202)
self.assertIn("X-Delete-Header", headers)
self.assertEqual(headers["X-Delete-Header"], "deleted")
self.assertIn("X-Request-Id", headers)

def test_delete_with_response_info_no_content(self):
"""Test delete_with_response_info with no content (returns boolean)"""
self.holodeck.mock(
Response(204, "", {"X-Delete-Header": "success"}),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Messages/MM123.json",
),
)
result, status_code, headers = self.client.api.v2010.delete_with_response_info(
method="DELETE", uri="/Accounts/AC123/Messages/MM123.json"
)

self.assertTrue(result)
self.assertIsInstance(result, bool)
self.assertEqual(status_code, 204)
self.assertIn("X-Delete-Header", headers)

def test_delete_with_response_info_invalid_json(self):
"""Test delete_with_response_info with invalid JSON content"""
self.holodeck.mock(
Response(
200,
"Not valid JSON",
{"X-Delete-Header": "deleted"},
),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Resources/DE123.json",
),
)
result, status_code, headers = self.client.api.v2010.delete_with_response_info(
method="DELETE", uri="/Accounts/AC123/Resources/DE123.json"
)

# Should fallback to boolean True when JSON parsing fails
self.assertTrue(result)
self.assertIsInstance(result, bool)
self.assertEqual(status_code, 200)
self.assertIn("X-Delete-Header", headers)

def test_create_with_response_info_error(self):
self.holodeck.mock(
Response(400, '{"message": "Invalid request"}'),
Expand Down Expand Up @@ -964,6 +1085,79 @@ async def test_delete_with_response_info_async_error(self):

self.assertIn("Unable to delete record", str(context.exception))

async def test_delete_with_response_info_async_json_body(self):
"""Test delete_with_response_info_async with JSON response body"""
self.holodeck.mock(
Response(
202,
'{"sid": "DE123", "status": "deleted", "account_sid": "AC123"}',
{"X-Delete-Header": "deleted", "X-Request-Id": "req123"},
),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Resources/DE123.json",
),
)
result, status_code, headers = (
await self.client.api.v2010.delete_with_response_info_async(
method="DELETE", uri="/Accounts/AC123/Resources/DE123.json"
)
)

self.assertIsInstance(result, dict)
self.assertEqual(result["sid"], "DE123")
self.assertEqual(result["status"], "deleted")
self.assertEqual(result["account_sid"], "AC123")
self.assertEqual(status_code, 202)
self.assertIn("X-Delete-Header", headers)
self.assertEqual(headers["X-Delete-Header"], "deleted")
self.assertIn("X-Request-Id", headers)

async def test_delete_with_response_info_async_no_content(self):
"""Test delete_with_response_info_async with no content (returns boolean)"""
self.holodeck.mock(
Response(204, "", {"X-Delete-Header": "success"}),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Messages/MM123.json",
),
)
result, status_code, headers = (
await self.client.api.v2010.delete_with_response_info_async(
method="DELETE", uri="/Accounts/AC123/Messages/MM123.json"
)
)

self.assertTrue(result)
self.assertIsInstance(result, bool)
self.assertEqual(status_code, 204)
self.assertIn("X-Delete-Header", headers)

async def test_delete_with_response_info_async_invalid_json(self):
"""Test delete_with_response_info_async with invalid JSON content"""
self.holodeck.mock(
Response(
200,
"Not valid JSON",
{"X-Delete-Header": "deleted"},
),
Request(
method="DELETE",
url="https://api.twilio.com/2010-04-01/Accounts/AC123/Resources/DE123.json",
),
)
result, status_code, headers = (
await self.client.api.v2010.delete_with_response_info_async(
method="DELETE", uri="/Accounts/AC123/Resources/DE123.json"
)
)

# Should fallback to boolean True when JSON parsing fails
self.assertTrue(result)
self.assertIsInstance(result, bool)
self.assertEqual(status_code, 200)
self.assertIn("X-Delete-Header", headers)

async def test_create_with_response_info_async_error(self):
"""Test create_with_response_info_async method with error"""
self.holodeck.mock(
Expand Down
46 changes: 31 additions & 15 deletions twilio/base/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,14 +382,28 @@
payload = self._parse_update(method, uri, response)
return payload, response.status_code, dict(response.headers or {})

def _parse_delete(self, method: str, uri: str, response: Response) -> bool:
def _parse_delete(self, method: str, uri: str, response: Response) -> Any:
"""
Parses delete response JSON
Parses delete response. Returns response body as dict if present, otherwise True.

For V1 APIs that return response bodies on delete (e.g., 202 with JSON),
this returns the parsed JSON payload. For traditional deletes (e.g., 204 No Content),
this returns True for backward compatibility.
"""
if response.status_code < 200 or response.status_code >= 300:
raise self.exception(method, uri, response, "Unable to delete record")

return True # if response code is 2XX, deletion was successful
# If response has content, parse and return it (V1 delete with response body)
if response.content and response.text:
try:
return json.loads(response.text)
except (ValueError, json.JSONDecodeError):

Check warning on line 400 in twilio/base/version.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this redundant Exception class; it derives from another which is already caught.

See more on https://sonarcloud.io/project/issues?id=twilio_twilio-python&issues=AZ0wA-5pz0CgNUl2czjr&open=AZ0wA-5pz0CgNUl2czjr&pullRequest=918
# If JSON parsing fails, fall back to boolean
pass

return (
True # Traditional delete: if response code is 2XX, deletion was successful
)

def delete(
self,
Expand Down Expand Up @@ -455,14 +469,15 @@
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
) -> Tuple[bool, int, Dict[str, str]]:
) -> Tuple[Union[Dict[str, Any], bool], int, Dict[str, str]]:
"""
Delete a resource and return response metadata

Returns:
tuple: (success_boolean, status_code, headers_dict)
- success_boolean: True if deletion was successful (2XX response)
- status_code: HTTP status code (typically 204 for successful delete)
tuple: (payload_or_success, status_code, headers_dict)
- payload_or_success: Response body dict if present (V1 APIs with response body),
or True boolean for traditional deletes (204 No Content)
- status_code: HTTP status code (typically 202 or 204 for successful delete)
- headers_dict: Response headers as a dictionary
"""
response = self.request(
Expand All @@ -475,8 +490,8 @@
timeout=timeout,
allow_redirects=allow_redirects,
)
success = self._parse_delete(method, uri, response)
return success, response.status_code, dict(response.headers or {})
result = self._parse_delete(method, uri, response)
return result, response.status_code, dict(response.headers or {})

async def delete_with_response_info_async(
self,
Expand All @@ -488,14 +503,15 @@
auth: Optional[Tuple[str, str]] = None,
timeout: Optional[float] = None,
allow_redirects: bool = False,
) -> Tuple[bool, int, Dict[str, str]]:
) -> Tuple[Union[Dict[str, Any], bool], int, Dict[str, str]]:
"""
Asynchronously delete a resource and return response metadata

Returns:
tuple: (success_boolean, status_code, headers_dict)
- success_boolean: True if deletion was successful (2XX response)
- status_code: HTTP status code (typically 204 for successful delete)
tuple: (payload_or_success, status_code, headers_dict)
- payload_or_success: Response body dict if present (V1 APIs with response body),
or True boolean for traditional deletes (204 No Content)
- status_code: HTTP status code (typically 202 or 204 for successful delete)
- headers_dict: Response headers as a dictionary
"""
response = await self.request_async(
Expand All @@ -508,8 +524,8 @@
timeout=timeout,
allow_redirects=allow_redirects,
)
success = self._parse_delete(method, uri, response)
return success, response.status_code, dict(response.headers or {})
result = self._parse_delete(method, uri, response)
return result, response.status_code, dict(response.headers or {})

def read_limits(
self, limit: Optional[int] = None, page_size: Optional[int] = None
Expand Down
Loading