mirror of
https://github.com/django/django.git
synced 2025-10-07 05:49:30 +00:00
When matching which entry in the `Accept` header should be used for a given media type, the specificity matters. However once those are resolved, only the quality matters when selecting preference. Regression in c075508b4de8edf9db553b409f8a8ed2f26ecead. Thank you to Anders Kaseorg for the report.
409 lines
15 KiB
Python
409 lines
15 KiB
Python
from unittest import TestCase
|
|
|
|
from django.http import HttpRequest
|
|
from django.http.request import MediaType
|
|
|
|
|
|
class MediaTypeTests(TestCase):
|
|
def test_empty(self):
|
|
for empty_media_type in (None, "", " "):
|
|
with self.subTest(media_type=empty_media_type):
|
|
media_type = MediaType(empty_media_type)
|
|
self.assertEqual(str(media_type), "")
|
|
self.assertEqual(repr(media_type), "<MediaType: >")
|
|
|
|
def test_str(self):
|
|
self.assertEqual(str(MediaType("*/*; q=0.8")), "*/*; q=0.8")
|
|
self.assertEqual(str(MediaType("application/xml")), "application/xml")
|
|
self.assertEqual(
|
|
str(MediaType("application/xml;type=madeup;q=42")),
|
|
"application/xml; type=madeup; q=42",
|
|
)
|
|
|
|
def test_repr(self):
|
|
self.assertEqual(repr(MediaType("*/*; q=0.8")), "<MediaType: */*; q=0.8>")
|
|
self.assertEqual(
|
|
repr(MediaType("application/xml")),
|
|
"<MediaType: application/xml>",
|
|
)
|
|
|
|
def test_match(self):
|
|
tests = [
|
|
("*/*; q=0.8", "*/*"),
|
|
("*/*", "application/json"),
|
|
(" */* ", "application/json"),
|
|
("application/*", "application/json"),
|
|
("application/*", "application/*"),
|
|
("application/xml", "application/xml"),
|
|
(" application/xml ", "application/xml"),
|
|
("application/xml", " application/xml "),
|
|
("text/vcard; version=4.0", "text/vcard; version=4.0"),
|
|
("text/vcard; version=4.0; q=0.7", "text/vcard; version=4.0"),
|
|
("text/vcard; version=4.0", "text/vcard"),
|
|
]
|
|
for accepted_type, mime_type in tests:
|
|
with self.subTest(accepted_type, mime_type=mime_type):
|
|
self.assertIs(MediaType(accepted_type).match(mime_type), True)
|
|
|
|
def test_no_match(self):
|
|
tests = [
|
|
# other is falsey.
|
|
("*/*", None),
|
|
("*/*", ""),
|
|
# other is malformed.
|
|
("*/*", "; q=0.8"),
|
|
# main_type is falsey.
|
|
("/*", "*/*"),
|
|
# other.main_type is falsey.
|
|
("*/*", "/*"),
|
|
# main sub_type is falsey.
|
|
("application", "application/*"),
|
|
# other.sub_type is falsey.
|
|
("application/*", "application"),
|
|
# All main and sub types are defined, but there is no match.
|
|
("application/xml", "application/html"),
|
|
("text/vcard; version=4.0", "text/vcard; version=3.0"),
|
|
("text/vcard; q=0.7", "text/vcard; version=3.0"),
|
|
("text/vcard", "text/vcard; version=3.0"),
|
|
]
|
|
for accepted_type, mime_type in tests:
|
|
with self.subTest(accepted_type, mime_type=mime_type):
|
|
self.assertIs(MediaType(accepted_type).match(mime_type), False)
|
|
|
|
def test_params(self):
|
|
tests = [
|
|
("text/plain", {}, {}),
|
|
("text/plain;q=0.7", {"q": "0.7"}, {}),
|
|
("text/plain;q=1.5", {"q": "1.5"}, {}),
|
|
("text/plain;q=xyz", {"q": "xyz"}, {}),
|
|
("text/plain;q=0.1234", {"q": "0.1234"}, {}),
|
|
("text/plain;version=2", {"version": "2"}, {"version": "2"}),
|
|
(
|
|
"text/plain;version=2;q=0.8",
|
|
{"version": "2", "q": "0.8"},
|
|
{"version": "2"},
|
|
),
|
|
(
|
|
"text/plain;q=0.8;version=2",
|
|
{"q": "0.8", "version": "2"},
|
|
{"version": "2"},
|
|
),
|
|
(
|
|
"text/plain; charset=UTF-8; q=0.3",
|
|
{"charset": "UTF-8", "q": "0.3"},
|
|
{"charset": "UTF-8"},
|
|
),
|
|
(
|
|
"text/plain ; q = 0.5 ; version = 3.0",
|
|
{"q": "0.5", "version": "3.0"},
|
|
{"version": "3.0"},
|
|
),
|
|
("text/plain; format=flowed", {"format": "flowed"}, {"format": "flowed"}),
|
|
(
|
|
"text/plain; format=flowed; q=0.4",
|
|
{"format": "flowed", "q": "0.4"},
|
|
{"format": "flowed"},
|
|
),
|
|
("text/*;q=0.2", {"q": "0.2"}, {}),
|
|
("*/json;q=0.9", {"q": "0.9"}, {}),
|
|
("application/json;q=0.9999", {"q": "0.9999"}, {}),
|
|
("text/html;q=0.0001", {"q": "0.0001"}, {}),
|
|
("text/html;q=0", {"q": "0"}, {}),
|
|
("text/html;q=0.", {"q": "0."}, {}),
|
|
("text/html;q=.8", {"q": ".8"}, {}),
|
|
("text/html;q= 0.9", {"q": "0.9"}, {}),
|
|
('text/html ; q = "0.6"', {"q": "0.6"}, {}),
|
|
]
|
|
for accepted_type, params, range_params in tests:
|
|
media_type = MediaType(accepted_type)
|
|
with self.subTest(accepted_type, attr="params"):
|
|
self.assertEqual(media_type.params, params)
|
|
with self.subTest(accepted_type, attr="range_params"):
|
|
self.assertEqual(media_type.range_params, range_params)
|
|
|
|
def test_quality(self):
|
|
tests = [
|
|
("*/*; q=0.8", 0.8),
|
|
("*/*; q=0.0001", 0),
|
|
("*/*; q=0.12345", 0.123),
|
|
("*/*; q=0.1", 0.1),
|
|
("*/*; q=-1", 1),
|
|
("*/*; q=2", 1),
|
|
("*/*; q=h", 1),
|
|
("*/*; q=inf", 1),
|
|
("*/*; q=0", 0),
|
|
("*/*", 1),
|
|
]
|
|
for accepted_type, quality in tests:
|
|
with self.subTest(accepted_type, quality=quality):
|
|
self.assertEqual(MediaType(accepted_type).quality, quality)
|
|
|
|
def test_specificity(self):
|
|
tests = [
|
|
("*/*", 0),
|
|
("*/*;q=0.5", 0),
|
|
("text/*", 1),
|
|
("text/*;q=0.5", 1),
|
|
("text/html", 2),
|
|
("text/html;q=1", 2),
|
|
("text/html;q=0.5", 2),
|
|
("text/html;version=5", 3),
|
|
]
|
|
for accepted_type, specificity in tests:
|
|
with self.subTest(accepted_type, specificity=specificity):
|
|
self.assertEqual(MediaType(accepted_type).specificity, specificity)
|
|
|
|
|
|
class AcceptHeaderTests(TestCase):
|
|
def test_no_headers(self):
|
|
"""Absence of Accept header defaults to '*/*'."""
|
|
request = HttpRequest()
|
|
self.assertEqual(
|
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
|
["*/*"],
|
|
)
|
|
|
|
def test_accept_headers(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/*,text/html, application/xhtml+xml,application/xml ;q=0.9,*/*;q=0.8,"
|
|
)
|
|
self.assertEqual(
|
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
|
[
|
|
"text/html",
|
|
"application/xhtml+xml",
|
|
"text/*",
|
|
"application/xml; q=0.9",
|
|
"*/*; q=0.8",
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
[
|
|
str(accepted_type)
|
|
for accepted_type in request.accepted_types_by_precedence
|
|
],
|
|
[
|
|
"text/html",
|
|
"application/xhtml+xml",
|
|
"application/xml; q=0.9",
|
|
"text/*",
|
|
"*/*; q=0.8",
|
|
],
|
|
)
|
|
|
|
def test_zero_quality(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "text/*;q=0,text/html"
|
|
self.assertEqual(
|
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
|
["text/html"],
|
|
)
|
|
|
|
def test_precedence(self):
|
|
"""
|
|
Taken from https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2.
|
|
"""
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/*, text/plain, text/plain;format=flowed, */*"
|
|
)
|
|
self.assertEqual(
|
|
[
|
|
str(accepted_type)
|
|
for accepted_type in request.accepted_types_by_precedence
|
|
],
|
|
[
|
|
"text/plain; format=flowed",
|
|
"text/plain",
|
|
"text/*",
|
|
"*/*",
|
|
],
|
|
)
|
|
|
|
def test_request_accepts_any(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "*/*"
|
|
self.assertIs(request.accepts("application/json"), True)
|
|
self.assertIsNone(request.get_preferred_type([]))
|
|
self.assertEqual(
|
|
request.get_preferred_type(["application/json", "text/plain"]),
|
|
"application/json",
|
|
)
|
|
|
|
def test_request_accepts_none(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = ""
|
|
self.assertIs(request.accepts("application/json"), False)
|
|
self.assertEqual(request.accepted_types, [])
|
|
self.assertIsNone(
|
|
request.get_preferred_type(["application/json", "text/plain"])
|
|
)
|
|
|
|
def test_request_accepts_some(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9"
|
|
)
|
|
self.assertIs(request.accepts("text/html"), True)
|
|
self.assertIs(request.accepts("application/xhtml+xml"), True)
|
|
self.assertIs(request.accepts("application/xml"), True)
|
|
self.assertIs(request.accepts("application/json"), False)
|
|
|
|
def test_accept_header_priority(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/html,application/xml;q=0.9,*/*;q=0.1,text/*;q=0.5"
|
|
)
|
|
|
|
tests = [
|
|
(["text/html", "application/xml"], "text/html"),
|
|
(["application/xml", "application/json"], "application/xml"),
|
|
(["application/json"], "application/json"),
|
|
(["application/json", "text/plain"], "text/plain"),
|
|
]
|
|
for types, preferred_type in tests:
|
|
with self.subTest(types, preferred_type=preferred_type):
|
|
self.assertEqual(str(request.get_preferred_type(types)), preferred_type)
|
|
|
|
def test_accept_header_priority_overlapping_mime(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "text/*;q=0.8,text/html;q=0.8"
|
|
|
|
self.assertEqual(
|
|
[str(accepted_type) for accepted_type in request.accepted_types],
|
|
[
|
|
"text/html; q=0.8",
|
|
"text/*; q=0.8",
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
[
|
|
str(accepted_type)
|
|
for accepted_type in request.accepted_types_by_precedence
|
|
],
|
|
[
|
|
"text/html; q=0.8",
|
|
"text/*; q=0.8",
|
|
],
|
|
)
|
|
|
|
def test_no_matching_accepted_type(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "text/html"
|
|
|
|
self.assertIsNone(
|
|
request.get_preferred_type(["application/json", "text/plain"])
|
|
)
|
|
|
|
def test_accept_with_param(self):
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "text/vcard; version=3.0, text/html;q=0.5"
|
|
|
|
for media_types, expected in [
|
|
(
|
|
[
|
|
"text/vcard; version=4.0",
|
|
"text/vcard; version=3.0",
|
|
"text/vcard",
|
|
"text/directory",
|
|
],
|
|
"text/vcard; version=3.0",
|
|
),
|
|
(["text/vcard; version=4.0", "text/vcard", "text/directory"], None),
|
|
(["text/vcard; version=4.0", "text/html"], "text/html"),
|
|
]:
|
|
self.assertEqual(request.get_preferred_type(media_types), expected)
|
|
|
|
def test_quality_for_media_type_rfc7231(self):
|
|
"""
|
|
Taken from https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2.
|
|
"""
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/*;q=0.3,text/html;q=0.7,text/html;level=1,text/html;level=2;q=0.4,"
|
|
"*/*;q=0.5"
|
|
)
|
|
|
|
for media_type, quality in [
|
|
("text/html;level=1", 1),
|
|
("text/html", 0.7),
|
|
("text/plain", 0.3),
|
|
("image/jpeg", 0.5),
|
|
("text/html;level=2", 0.4),
|
|
("text/html;level=3", 0.7),
|
|
]:
|
|
with self.subTest(media_type):
|
|
accepted_media_type = request.accepted_type(media_type)
|
|
self.assertIsNotNone(accepted_media_type)
|
|
self.assertEqual(accepted_media_type.quality, quality)
|
|
|
|
for media_types, expected in [
|
|
(["text/html", "text/html; level=1"], "text/html; level=1"),
|
|
(["text/html; level=2", "text/html; level=3"], "text/html; level=3"),
|
|
]:
|
|
self.assertEqual(request.get_preferred_type(media_types), expected)
|
|
|
|
def test_quality_for_media_type_rfc9110(self):
|
|
"""
|
|
Taken from https://www.rfc-editor.org/rfc/rfc9110.html#section-12.5.1-18.
|
|
"""
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = (
|
|
"text/*;q=0.3, text/plain;q=0.7, text/plain;format=flowed, "
|
|
"text/plain;format=fixed;q=0.4, */*;q=0.5"
|
|
)
|
|
|
|
for media_type, quality in [
|
|
("text/plain;format=flowed", 1),
|
|
("text/plain", 0.7),
|
|
("text/html", 0.3),
|
|
("image/jpeg", 0.5),
|
|
("text/plain;format=fixed", 0.4),
|
|
("text/html;level=3", 0.3), # https://www.rfc-editor.org/errata/eid7138
|
|
]:
|
|
with self.subTest(media_type):
|
|
accepted_media_type = request.accepted_type(media_type)
|
|
self.assertIsNotNone(accepted_media_type)
|
|
self.assertEqual(accepted_media_type.quality, quality)
|
|
|
|
for media_types, expected in [
|
|
(["text/plain", "text/plain; format=flowed"], "text/plain; format=flowed"),
|
|
(["text/html", "image/jpeg"], "image/jpeg"),
|
|
]:
|
|
self.assertEqual(request.get_preferred_type(media_types), expected)
|
|
|
|
def test_quality_breaks_specificity(self):
|
|
"""
|
|
With the same specificity, the quality breaks the tie.
|
|
"""
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "text/plain;q=0.5,text/html"
|
|
|
|
self.assertEqual(request.accepted_type("text/plain").quality, 0.5)
|
|
self.assertEqual(request.accepted_type("text/plain").specificity, 2)
|
|
|
|
self.assertEqual(request.accepted_type("text/html").quality, 1)
|
|
self.assertEqual(request.accepted_type("text/html").specificity, 2)
|
|
|
|
self.assertEqual(
|
|
request.get_preferred_type(["text/html", "text/plain"]), "text/html"
|
|
)
|
|
|
|
def test_quality_over_specificity(self):
|
|
"""
|
|
For media types with the same quality, prefer the more specific type.
|
|
"""
|
|
request = HttpRequest()
|
|
request.META["HTTP_ACCEPT"] = "text/*,image/jpeg"
|
|
|
|
self.assertEqual(request.accepted_type("text/plain").quality, 1)
|
|
self.assertEqual(request.accepted_type("text/plain").specificity, 1)
|
|
|
|
self.assertEqual(request.accepted_type("image/jpeg").quality, 1)
|
|
self.assertEqual(request.accepted_type("image/jpeg").specificity, 2)
|
|
|
|
self.assertEqual(
|
|
request.get_preferred_type(["text/plain", "image/jpeg"]), "image/jpeg"
|
|
)
|