Skip to content

Commit 6afcba5

Browse files
authored
integrity: refine Accept header handling (#17498)
* integrity: refine Accept header handling See #17084. Signed-off-by: William Woodruff <[email protected]> * remove unneeded identity fallback Signed-off-by: William Woodruff <[email protected]> * remove unused MIME types Signed-off-by: William Woodruff <[email protected]> * remove HTML mime type uses Signed-off-by: William Woodruff <[email protected]> --------- Signed-off-by: William Woodruff <[email protected]>
1 parent b70d12b commit 6afcba5

File tree

3 files changed

+63
-16
lines changed

3 files changed

+63
-16
lines changed

tests/unit/api/test_integrity.py

+56-7
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,59 @@
1616
from warehouse.api import integrity
1717

1818

19-
def test_select_content_type(db_request):
20-
db_request.accept = "application/json"
19+
@pytest.mark.parametrize(
20+
("accept", "expected"),
21+
[
22+
# Simple cases
23+
(
24+
"application/vnd.pypi.integrity.v1+json",
25+
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
26+
),
27+
("application/json", integrity.MIME_APPLICATION_JSON),
28+
# No accept header means we give the user our first offer
29+
(None, integrity.MIME_PYPI_INTEGRITY_V1_JSON),
30+
# Accept header contains only things we don't offer
31+
("text/xml", None),
32+
("application/octet-stream", None),
33+
("text/xml, application/octet-stream", None),
34+
# Accept header contains both things we offer and things we don't;
35+
# we pick our matching offer even if the q-value is lower
36+
(
37+
"text/xml, application/vnd.pypi.integrity.v1+json",
38+
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
39+
),
40+
(
41+
"application/vnd.pypi.integrity.v1+json; q=0.1, text/xml",
42+
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
43+
),
44+
# Accept header contains multiple things we offer with the same q-value;
45+
# we pick our preferred offer
46+
(
47+
"application/json, application/vnd.pypi.integrity.v1+json",
48+
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
49+
),
50+
(
51+
"application/vnd.pypi.integrity.v1+json; q=0.5, application/json; q=0.5",
52+
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
53+
),
54+
# Accept header contains multiple things we offer; we pick our
55+
# offer based on the q-value
56+
(
57+
"application/vnd.pypi.integrity.v1+json; q=0.1, application/json",
58+
integrity.MIME_APPLICATION_JSON,
59+
),
60+
],
61+
)
62+
def test_select_content_type(db_request, accept, expected):
63+
db_request.accept = accept
2164

22-
assert (
23-
integrity._select_content_type(db_request)
24-
== integrity.MIME_PYPI_INTEGRITY_V1_JSON
25-
)
65+
assert integrity._select_content_type(db_request) == expected
2666

2767

2868
# Backstop; can be removed/changed once this view supports HTML.
2969
@pytest.mark.parametrize(
3070
"content_type",
31-
[integrity.MIME_TEXT_HTML, integrity.MIME_PYPI_INTEGRITY_V1_HTML],
71+
["text/html", "application/vnd.pypi.integrity.v1+html"],
3272
)
3373
def test_provenance_for_file_bad_accept(db_request, content_type):
3474
db_request.accept = content_type
@@ -37,6 +77,15 @@ def test_provenance_for_file_bad_accept(db_request, content_type):
3777
assert response.json == {"message": "Request not acceptable"}
3878

3979

80+
def test_provenance_for_file_accept_multiple(db_request, monkeypatch):
81+
db_request.accept = "text/html, application/vnd.pypi.integrity.v1+json; q=0.9"
82+
file = pretend.stub(provenance=None, filename="fake-1.2.3.tar.gz")
83+
84+
response = integrity.provenance_for_file(file, db_request)
85+
assert response.status_code == 404
86+
assert response.json == {"message": "No provenance available for fake-1.2.3.tar.gz"}
87+
88+
4089
def test_provenance_for_file_not_enabled(db_request, monkeypatch):
4190
monkeypatch.setattr(db_request, "flags", pretend.stub(enabled=lambda *a: True))
4291

warehouse/api/integrity.py

+6-8
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,22 @@
2020
from warehouse.packaging.models import File
2121
from warehouse.utils.cors import _CORS_HEADERS
2222

23-
MIME_TEXT_HTML = "text/html"
24-
MIME_PYPI_INTEGRITY_V1_HTML = "application/vnd.pypi.integrity.v1+html"
23+
MIME_APPLICATION_JSON = "application/json"
2524
MIME_PYPI_INTEGRITY_V1_JSON = "application/vnd.pypi.integrity.v1+json"
2625

2726

28-
def _select_content_type(request: Request) -> str:
27+
def _select_content_type(request: Request) -> str | None:
2928
offers = request.accept.acceptable_offers(
3029
[
3130
# JSON currently has the highest priority.
3231
MIME_PYPI_INTEGRITY_V1_JSON,
33-
MIME_TEXT_HTML,
34-
MIME_PYPI_INTEGRITY_V1_HTML,
32+
MIME_APPLICATION_JSON,
3533
]
3634
)
3735

38-
# Default case: JSON.
36+
# Client provided an Accept header, but none of the offers matched.
3937
if not offers:
40-
return MIME_PYPI_INTEGRITY_V1_JSON
38+
return None
4139
else:
4240
return offers[0][0]
4341

@@ -63,7 +61,7 @@ def provenance_for_file(file: File, request: Request):
6361
# Determine our response content-type. For the time being, only the JSON
6462
# type is accepted.
6563
request.response.content_type = _select_content_type(request)
66-
if request.response.content_type != MIME_PYPI_INTEGRITY_V1_JSON:
64+
if not request.response.content_type:
6765
return HTTPNotAcceptable(json={"message": "Request not acceptable"})
6866

6967
if request.flags.enabled(AdminFlagValue.DISABLE_PEP740):

warehouse/api/simple.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def _select_content_type(request: Request) -> str:
5050
]
5151
)
5252

53-
# Default case, we want to return whatevr we want to return
53+
# Default case, we want to return whatever we want to return
5454
# by default when there is no Accept header.
5555
if not offers:
5656
return MIME_TEXT_HTML

0 commit comments

Comments
 (0)