diff --git a/tests/unit/base/test_version.py b/tests/unit/base/test_version.py index 94c0ad496..03c684a2f 100644 --- a/tests/unit/base/test_version.py +++ b/tests/unit/base/test_version.py @@ -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""" @@ -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"}'), @@ -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( diff --git a/twilio/base/version.py b/twilio/base/version.py index 6d7360708..3f37d9e88 100644 --- a/twilio/base/version.py +++ b/twilio/base/version.py @@ -382,14 +382,28 @@ async def update_with_response_info_async( 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): + # If JSON parsing fails, fall back to boolean + pass + + return ( + True # Traditional delete: if response code is 2XX, deletion was successful + ) def delete( self, @@ -455,14 +469,15 @@ def delete_with_response_info( 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( @@ -475,8 +490,8 @@ def delete_with_response_info( 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, @@ -488,14 +503,15 @@ async def delete_with_response_info_async( 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( @@ -508,8 +524,8 @@ async def delete_with_response_info_async( 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