Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integrity: refine Accept header handling #17498

Merged
merged 4 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
61 changes: 55 additions & 6 deletions tests/unit/api/test_integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,53 @@
from warehouse.api import integrity


def test_select_content_type(db_request):
db_request.accept = "application/json"
@pytest.mark.parametrize(
("accept", "expected"),
[
# Simple cases
(
"application/vnd.pypi.integrity.v1+json",
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
),
("application/json", integrity.MIME_APPLICATION_JSON),
# No accept header means we give the user our first offer
(None, integrity.MIME_PYPI_INTEGRITY_V1_JSON),
# Accept header contains only things we don't offer
("text/xml", None),
("application/octet-stream", None),
("text/xml, application/octet-stream", None),
# Accept header contains both things we offer and things we don't;
# we pick our matching offer even if the q-value is lower
(
"text/xml, application/vnd.pypi.integrity.v1+json",
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
),
(
"application/vnd.pypi.integrity.v1+json; q=0.1, text/xml",
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
),
# Accept header contains multiple things we offer with the same q-value;
# we pick our preferred offer
(
"application/json, application/vnd.pypi.integrity.v1+json",
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
),
(
"application/vnd.pypi.integrity.v1+json; q=0.5, application/json; q=0.5",
integrity.MIME_PYPI_INTEGRITY_V1_JSON,
),
# Accept header contains multiple things we offer; we pick our
# offer based on the q-value
(
"application/vnd.pypi.integrity.v1+json; q=0.1, application/json",
integrity.MIME_APPLICATION_JSON,
),
],
)
def test_select_content_type(db_request, accept, expected):
db_request.accept = accept

assert (
integrity._select_content_type(db_request)
== integrity.MIME_PYPI_INTEGRITY_V1_JSON
)
assert integrity._select_content_type(db_request) == expected


# Backstop; can be removed/changed once this view supports HTML.
Expand All @@ -37,6 +77,15 @@ def test_provenance_for_file_bad_accept(db_request, content_type):
assert response.json == {"message": "Request not acceptable"}


def test_provenance_for_file_accept_multiple(db_request, monkeypatch):
db_request.accept = "text/html, application/vnd.pypi.integrity.v1+json; q=0.9"
file = pretend.stub(provenance=None, filename="fake-1.2.3.tar.gz")

response = integrity.provenance_for_file(file, db_request)
assert response.status_code == 404
assert response.json == {"message": "No provenance available for fake-1.2.3.tar.gz"}


def test_provenance_for_file_not_enabled(db_request, monkeypatch):
monkeypatch.setattr(db_request, "flags", pretend.stub(enabled=lambda *a: True))

Expand Down
15 changes: 9 additions & 6 deletions warehouse/api/integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,26 @@
from warehouse.utils.cors import _CORS_HEADERS

MIME_TEXT_HTML = "text/html"
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
MIME_APPLICATION_JSON = "application/json"
MIME_PYPI_INTEGRITY_V1_HTML = "application/vnd.pypi.integrity.v1+html"
MIME_PYPI_INTEGRITY_V1_JSON = "application/vnd.pypi.integrity.v1+json"


def _select_content_type(request: Request) -> str:
def _select_content_type(request: Request) -> str | None:
offers = request.accept.acceptable_offers(
[
# JSON currently has the highest priority.
MIME_PYPI_INTEGRITY_V1_JSON,
MIME_TEXT_HTML,
MIME_PYPI_INTEGRITY_V1_HTML,
MIME_APPLICATION_JSON,
# Ensures that we fall back to the first offer if
# the client does not provide an Accept header.
"identity",
woodruffw marked this conversation as resolved.
Show resolved Hide resolved
]
)

# Default case: JSON.
# Client provided an Accept header, but none of the offers matched.
if not offers:
return MIME_PYPI_INTEGRITY_V1_JSON
return None
else:
return offers[0][0]

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

if request.flags.enabled(AdminFlagValue.DISABLE_PEP740):
Expand Down
2 changes: 1 addition & 1 deletion warehouse/api/simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def _select_content_type(request: Request) -> str:
]
)

# Default case, we want to return whatevr we want to return
# Default case, we want to return whatever we want to return
# by default when there is no Accept header.
if not offers:
return MIME_TEXT_HTML
Expand Down