Skip to content
Merged
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
25 changes: 25 additions & 0 deletions apps/downloads/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,31 @@ def test_v1_release_file_delete_by_release_still_works(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(len(self.get_json(response)), 0)

def test_unsupported_request_content_type_returns_415(self):
url = self.create_url("os")
cases = [
("application/yaml", "name: Unsupported OS\nslug: unsupported-os\n"),
("application/x-www-form-urlencoded", "name=Unsupported+OS&slug=unsupported-os"),
]

for content_type, body in cases:
with self.subTest(content_type=content_type):
response = self.client.post(
url, body, content_type=content_type, headers={"authorization": self.Authorization}
)

self.assertEqual(response.status_code, 415)
self.assertIn(content_type, response.json()["error"])
self.assertFalse(OS.objects.filter(slug="unsupported-os").exists())

def test_non_integer_detail_lookup_returns_404(self):
url = self.create_url("release", "latest")

response = self.client.get(url)

self.assertEqual(response.status_code, 404)
self.assertEqual(response.json()["error"], "Invalid resource lookup data provided (mismatched type).")


class DownloadApiV2ViewsTest(BaseDownloadApiViewsTest, BaseDownloadTests, APITestCase):
api_version = "v2"
Expand Down
24 changes: 21 additions & 3 deletions pydotorg/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from django.contrib.auth.models import AnonymousUser
from tastypie.authentication import ApiKeyAuthentication
from tastypie.authorization import Authorization
from tastypie.exceptions import Unauthorized
from tastypie.http import HttpUnauthorized
from tastypie.resources import ModelResource
from tastypie.exceptions import ImmediateHttpResponse, NotFound, Unauthorized, UnsupportedDeserializationFormat
from tastypie.http import HttpNotFound, HttpUnauthorized, HttpUnsupportedMediaType
from tastypie.resources import ModelResource, sanitize
from tastypie.throttle import CacheThrottle

API_KEY_AUTHENTICATED_ATTR = "_pydotorg_api_key_authenticated"
Expand Down Expand Up @@ -184,3 +184,21 @@ class Meta:
detail_allowed_methods = ["get", "delete"]
throttle = CacheThrottle(throttle_at=600) # default is 150 req/hr
abstract = True

def deserialize(self, request, data, format="application/json"): # noqa: A002 - tastypie hook signature
"""Return a 415 for unsupported request content types without firing the 500 signal."""
try:
return super().deserialize(request, data, format=format)
except UnsupportedDeserializationFormat as exc:
response = self.error_response(
request, {"error": sanitize(str(exc))}, response_class=HttpUnsupportedMediaType
)
raise ImmediateHttpResponse(response=response) from exc

def get_detail(self, request, **kwargs):
"""Return a 404 for malformed detail lookups without firing the 500 signal."""
try:
return super().get_detail(request, **kwargs)
except NotFound as exc:
response = self.error_response(request, {"error": sanitize(str(exc))}, response_class=HttpNotFound)
raise ImmediateHttpResponse(response=response) from exc
Loading