Skip to content

Commit 1266a90

Browse files
committed
feat: add api v1 standard error response
1 parent bbe5c74 commit 1266a90

File tree

3 files changed

+70
-299
lines changed

3 files changed

+70
-299
lines changed

tests/unit/base/test_version.py

Lines changed: 38 additions & 179 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import json
21
import unittest
32
from unittest.mock import Mock
43

54
from tests import IntegrationTestCase
65
from tests.holodeck import Request
7-
from twilio.base.api_v1_version import ApiV1Version
8-
from twilio.base.exceptions import TwilioServiceException
6+
from twilio.base.version import Version
7+
from twilio.base.exceptions import TwilioRestException, TwilioServiceException
98
from twilio.base.page import Page
109
from twilio.http.response import Response
1110

@@ -127,11 +126,11 @@ def test_delete_not_found(self):
127126
self.assertIn("Unable to delete record", str(context.exception))
128127

129128

130-
class ExceptionTestCase(unittest.TestCase):
131-
"""Test cases for ApiV1Version.exception() method"""
129+
class VersionExceptionTestCase(unittest.TestCase):
130+
"""Test cases for base Version.exception() method with RFC-9457 auto-detection"""
132131

133-
def test_exception_rfc9457_compliant(self):
134-
"""Test that RFC-9457 compliant errors create TwilioServiceException"""
132+
def test_exception_rfc9457_auto_detection(self):
133+
"""Test that base Version auto-detects RFC-9457 errors and creates TwilioServiceException"""
135134
response = Mock(spec=Response)
136135
response.status_code = 400
137136
response.text = """{
@@ -143,9 +142,8 @@ def test_exception_rfc9457_compliant(self):
143142
"instance": "/api/v1/accounts/AC123/calls/CA456"
144143
}"""
145144

146-
error_payload = json.loads(response.text)
147-
exception = ApiV1Version.exception(
148-
method="POST", uri="/test", response=response, error_payload=error_payload
145+
exception = Version.exception(
146+
method="POST", uri="/test", response=response, message="Test error"
149147
)
150148

151149
self.assertIsInstance(exception, TwilioServiceException)
@@ -158,8 +156,26 @@ def test_exception_rfc9457_compliant(self):
158156
self.assertEqual(exception.method, "POST")
159157
self.assertEqual(exception.uri, "/test")
160158

161-
def test_exception_rfc9457_with_validation_errors(self):
162-
"""Test RFC-9457 error with validation errors array"""
159+
def test_exception_legacy_format_fallback(self):
160+
"""Test that base Version falls back to TwilioRestException for legacy errors"""
161+
response = Mock(spec=Response)
162+
response.status_code = 400
163+
response.text = """{
164+
"message": "Invalid phone number",
165+
"code": 21211
166+
}"""
167+
168+
exception = Version.exception(
169+
method="POST", uri="/test", response=response, message="Test error"
170+
)
171+
172+
self.assertIsInstance(exception, TwilioRestException)
173+
self.assertEqual(exception.status, 400)
174+
self.assertEqual(exception.code, 21211)
175+
self.assertIn("Invalid phone number", exception.msg)
176+
177+
def test_exception_rfc9457_with_validation_errors_base_version(self):
178+
"""Test base Version handles RFC-9457 with validation errors array"""
163179
response = Mock(spec=Response)
164180
response.status_code = 422
165181
response.text = """{
@@ -174,182 +190,25 @@ def test_exception_rfc9457_with_validation_errors(self):
174190
]
175191
}"""
176192

177-
error_payload = json.loads(response.text)
178-
exception = ApiV1Version.exception(
179-
method="POST", uri="/test", response=response, error_payload=error_payload
193+
exception = Version.exception(
194+
method="POST", uri="/test", response=response, message="Validation error"
180195
)
181196

182197
self.assertIsInstance(exception, TwilioServiceException)
183198
self.assertEqual(exception.title, "Validation failed")
184199
self.assertEqual(len(exception.errors), 2)
185200
self.assertEqual(exception.errors[0]["detail"], "must be a positive integer")
186201
self.assertEqual(exception.errors[0]["pointer"], "#/age")
187-
self.assertEqual(
188-
exception.errors[1]["detail"], "must be 'green', 'red' or 'blue'"
189-
)
190-
self.assertEqual(exception.errors[1]["pointer"], "#/profile/color")
191202

192-
def test_exception_rfc9457_minimal(self):
193-
"""Test RFC-9457 with only required fields"""
203+
def test_exception_malformed_json_fallback(self):
204+
"""Test that base Version handles malformed JSON gracefully"""
194205
response = Mock(spec=Response)
195-
response.status_code = 404
196-
response.text = """{
197-
"type": "https://www.twilio.com/docs/api/errors/20404",
198-
"title": "Resource not found",
199-
"status": 404,
200-
"code": 20404
201-
}"""
202-
203-
error_payload = json.loads(response.text)
204-
exception = ApiV1Version.exception(
205-
method="GET", uri="/test", response=response, error_payload=error_payload
206-
)
207-
208-
self.assertIsInstance(exception, TwilioServiceException)
209-
self.assertEqual(exception.title, "Resource not found")
210-
self.assertIsNone(exception.detail)
211-
self.assertIsNone(exception.instance)
212-
self.assertEqual(exception.errors, [])
213-
214-
def test_exception_with_error_payload_parameter(self):
215-
"""Test passing error_payload directly to exception method"""
216-
response = Mock(spec=Response)
217-
response.status_code = 400
218-
response.text = "This should be ignored"
219-
220-
error_payload = {
221-
"type": "https://www.twilio.com/docs/api/errors/20001",
222-
"title": "Invalid parameter",
223-
"status": 400,
224-
"code": 20001,
225-
"detail": "Pre-parsed error payload",
226-
}
206+
response.status_code = 500
207+
response.text = "This is not JSON"
227208

228-
exception = ApiV1Version.exception(
229-
method="POST", uri="/test", response=response, error_payload=error_payload
209+
exception = Version.exception(
210+
method="GET", uri="/test", response=response, message="Server error"
230211
)
231212

232-
self.assertIsInstance(exception, TwilioServiceException)
233-
self.assertEqual(exception.title, "Invalid parameter")
234-
self.assertEqual(exception.detail, "Pre-parsed error payload")
235-
236-
237-
class ApiV1VersionParseTestCase(unittest.TestCase):
238-
"""Test cases for ApiV1Version._parse_* methods with RFC-9457 error handling"""
239-
240-
def test_parse_fetch_with_rfc9457_error(self):
241-
"""Test that _parse_fetch raises TwilioServiceException for RFC-9457 errors"""
242-
# Create a mock domain
243-
domain = Mock()
244-
service_version = ApiV1Version(domain, "v1")
245-
246-
response = Mock(spec=Response)
247-
response.status_code = 404
248-
response.text = """{
249-
"type": "https://www.twilio.com/docs/api/errors/20404",
250-
"title": "Resource not found",
251-
"status": 404,
252-
"code": 20404,
253-
"detail": "The requested resource does not exist"
254-
}"""
255-
256-
with self.assertRaises(TwilioServiceException) as context:
257-
service_version._parse_fetch("GET", "/test", response)
258-
259-
exception = context.exception
260-
self.assertEqual(exception.title, "Resource not found")
261-
self.assertEqual(exception.detail, "The requested resource does not exist")
262-
self.assertEqual(exception.status, 404)
263-
264-
def test_parse_update_with_rfc9457_error(self):
265-
"""Test that _parse_update raises TwilioServiceException for RFC-9457 errors"""
266-
domain = Mock()
267-
service_version = ApiV1Version(domain, "v1")
268-
269-
response = Mock(spec=Response)
270-
response.status_code = 400
271-
response.text = """{
272-
"type": "https://www.twilio.com/docs/api/errors/20001",
273-
"title": "Invalid parameter",
274-
"status": 400,
275-
"code": 20001
276-
}"""
277-
278-
with self.assertRaises(TwilioServiceException) as context:
279-
service_version._parse_update("PUT", "/test", response)
280-
281-
exception = context.exception
282-
self.assertEqual(exception.title, "Invalid parameter")
283-
284-
def test_parse_delete_with_rfc9457_error(self):
285-
"""Test that _parse_delete raises TwilioServiceException for RFC-9457 errors"""
286-
domain = Mock()
287-
service_version = ApiV1Version(domain, "v1")
288-
289-
response = Mock(spec=Response)
290-
response.status_code = 403
291-
response.text = """{
292-
"type": "https://www.twilio.com/docs/api/errors/20403",
293-
"title": "Forbidden",
294-
"status": 403,
295-
"code": 20403,
296-
"detail": "You do not have permission to delete this resource"
297-
}"""
298-
299-
with self.assertRaises(TwilioServiceException) as context:
300-
service_version._parse_delete("DELETE", "/test", response)
301-
302-
exception = context.exception
303-
self.assertEqual(exception.title, "Forbidden")
304-
self.assertEqual(
305-
exception.detail, "You do not have permission to delete this resource"
306-
)
307-
308-
def test_parse_create_with_rfc9457_error(self):
309-
"""Test that _parse_create raises TwilioServiceException for RFC-9457 errors"""
310-
domain = Mock()
311-
service_version = ApiV1Version(domain, "v1")
312-
313-
response = Mock(spec=Response)
314-
response.status_code = 422
315-
response.text = """{
316-
"type": "https://www.twilio.com/docs/api/errors/20001",
317-
"title": "Validation failed",
318-
"status": 422,
319-
"code": 20001,
320-
"errors": [
321-
{"detail": "must be a valid phone number", "pointer": "#/to"}
322-
]
323-
}"""
324-
325-
with self.assertRaises(TwilioServiceException) as context:
326-
service_version._parse_create("POST", "/test", response)
327-
328-
exception = context.exception
329-
self.assertEqual(exception.title, "Validation failed")
330-
self.assertEqual(len(exception.errors), 1)
331-
self.assertEqual(exception.errors[0]["detail"], "must be a valid phone number")
332-
333-
def test_parse_fetch_success(self):
334-
"""Test that _parse_fetch returns data on success"""
335-
domain = Mock()
336-
service_version = ApiV1Version(domain, "v1")
337-
338-
response = Mock(spec=Response)
339-
response.status_code = 200
340-
response.text = '{"data": "success"}'
341-
342-
result = service_version._parse_fetch("GET", "/test", response)
343-
self.assertEqual(result, {"data": "success"})
344-
345-
def test_parse_delete_success(self):
346-
"""Test that _parse_delete returns True on success"""
347-
domain = Mock()
348-
service_version = ApiV1Version(domain, "v1")
349-
350-
response = Mock(spec=Response)
351-
response.status_code = 204
352-
response.text = ""
353-
354-
result = service_version._parse_delete("DELETE", "/test", response)
355-
self.assertTrue(result)
213+
self.assertIsInstance(exception, TwilioRestException)
214+
self.assertEqual(exception.status, 500)

twilio/base/api_v1_version.py

Lines changed: 0 additions & 109 deletions
This file was deleted.

0 commit comments

Comments
 (0)