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
17 changes: 17 additions & 0 deletions src/mock_vws/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@
b"\xaeB`\x82"
)

VUMARK_SVG = (
b'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>'
)

VUMARK_PDF = (
b"%PDF-1.4\n"
b"1 0 obj<</Type /Catalog /Pages 2 0 R>>endobj\n"
b"2 0 obj<</Type /Pages /Kids [3 0 R] /Count 1>>endobj\n"
b"3 0 obj<</Type /Page /MediaBox [0 0 100 100]>>endobj\n"
b"xref\n0 4\n"
b"0000000000 65535 f \n"
b"trailer<</Size 4/Root 1 0 R>>\n"
b"startxref\n9\n%%EOF"
)


@beartype
@unique
Expand Down Expand Up @@ -45,6 +60,8 @@ class ResultCodes(Enum):
PROJECT_INACTIVE = "ProjectInactive"
INACTIVE_PROJECT = "InactiveProject"
TOO_MANY_REQUESTS = "TooManyRequests"
INVALID_ACCEPT_HEADER = "InvalidAcceptHeader"
INVALID_INSTANCE_ID = "InvalidInstanceId"


@beartype
Expand Down
31 changes: 28 additions & 3 deletions src/mock_vws/_flask_server/vws.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@
from flask import Flask, Response, request
from pydantic_settings import BaseSettings

from mock_vws._constants import VUMARK_PNG, ResultCodes, TargetStatuses
from mock_vws._constants import (
VUMARK_PDF,
VUMARK_PNG,
VUMARK_SVG,
ResultCodes,
TargetStatuses,
)
from mock_vws._database_matchers import get_database_matching_server_keys
from mock_vws._mock_common import json_dump
from mock_vws._services_validators import run_services_validators
from mock_vws._services_validators.exceptions import (
FailError,
InvalidAcceptHeaderError,
InvalidInstanceIdError,
TargetStatusNotSuccessError,
TargetStatusProcessingError,
ValidatorError,
Expand Down Expand Up @@ -351,10 +359,27 @@ def generate_vumark_instance(target_id: str) -> Response:
"""
# ``target_id`` is validated by request validators.
del target_id

accept = request.headers.get(key="Accept", default="")
valid_accept_types: dict[str, bytes] = {
"image/png": VUMARK_PNG,
"image/svg+xml": VUMARK_SVG,
"application/pdf": VUMARK_PDF,
}
if accept not in valid_accept_types:
raise InvalidAcceptHeaderError

request_json = json.loads(s=request.data)
instance_id = request_json.get("instance_id", "")
if not instance_id:
raise InvalidInstanceIdError

response_body = valid_accept_types[accept]
content_type = accept
date = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
headers = {
"Connection": "keep-alive",
"Content-Type": "image/png",
"Content-Type": content_type,
"server": "envoy",
"Date": date,
"x-envoy-upstream-service-time": "5",
Expand All @@ -364,7 +389,7 @@ def generate_vumark_instance(target_id: str) -> Response:
}
return Response(
status=HTTPStatus.OK,
response=VUMARK_PNG,
response=response_body,
headers=headers,
)

Expand Down
47 changes: 37 additions & 10 deletions src/mock_vws/_requests_mock_server/mock_web_services_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@
from beartype import BeartypeConf, beartype
from requests.models import PreparedRequest

from mock_vws._constants import VUMARK_PNG, ResultCodes, TargetStatuses
from mock_vws._constants import (
VUMARK_PDF,
VUMARK_PNG,
VUMARK_SVG,
ResultCodes,
TargetStatuses,
)
from mock_vws._database_matchers import get_database_matching_server_keys
from mock_vws._mock_common import Route, json_dump
from mock_vws._services_validators import run_services_validators
from mock_vws._services_validators.exceptions import (
FailError,
InvalidAcceptHeaderError,
InvalidInstanceIdError,
TargetStatusNotSuccessError,
TargetStatusProcessingError,
ValidatorError,
Expand Down Expand Up @@ -295,30 +303,49 @@ def generate_vumark_instance(
self, request: PreparedRequest
) -> _ResponseType:
"""Generate a VuMark instance."""
run_services_validators(
request_headers=request.headers,
request_body=_body_bytes(request=request),
request_method=request.method or "",
request_path=request.path_url,
databases=self._target_manager.databases,
)
valid_accept_types: dict[str, bytes] = {
"image/png": VUMARK_PNG,
"image/svg+xml": VUMARK_SVG,
"application/pdf": VUMARK_PDF,
}
try:
run_services_validators(
request_headers=request.headers,
request_body=_body_bytes(request=request),
request_method=request.method or "",
request_path=request.path_url,
databases=self._target_manager.databases,
)

accept = dict(request.headers).get("Accept", "")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case-insensitive header lookup lost by dict conversion

Low Severity

dict(request.headers).get("Accept", "") converts the CaseInsensitiveDict from PreparedRequest.headers into a plain dict, losing case-insensitive lookup. This means a header sent as e.g. accept: image/png won't be found, incorrectly triggering InvalidAcceptHeaderError. The Flask backend at the equivalent point uses request.headers.get(key="Accept", default="") which preserves case-insensitive behavior, creating an inconsistency between the two mock backends.

Fix in Cursor Fix in Web

if accept not in valid_accept_types:
raise InvalidAcceptHeaderError

request_json = json.loads(s=_body_bytes(request=request))
instance_id = request_json.get("instance_id", "")
if not instance_id:
raise InvalidInstanceIdError
except ValidatorError as exc:
return exc.status_code, exc.headers, exc.response_text

response_body = valid_accept_types[accept]
content_type = accept
date = email.utils.formatdate(
timeval=None,
localtime=False,
usegmt=True,
)
headers = {
"Connection": "keep-alive",
"Content-Type": "image/png",
"Content-Type": content_type,
"Date": date,
"server": "envoy",
"x-envoy-upstream-service-time": "5",
"strict-transport-security": "max-age=31536000",
"x-aws-region": "us-east-2, us-west-2",
"x-content-type-options": "nosniff",
}
return HTTPStatus.OK, headers, VUMARK_PNG
return HTTPStatus.OK, headers, response_body

@route(path_pattern="/summary", http_methods={HTTPMethod.GET})
def database_summary(self, request: PreparedRequest) -> _ResponseType:
Expand Down
76 changes: 76 additions & 0 deletions src/mock_vws/_services_validators/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,82 @@ def __init__(self) -> None:
}


@beartype
class InvalidAcceptHeaderError(ValidatorError):
"""Exception raised when an unsupported Accept header is given."""

def __init__(self) -> None:
"""
Attributes:
status_code: The status code to use in a response if this is
raised.
response_text: The response text to use in a response if this
is
raised.
"""
super().__init__()
self.status_code = HTTPStatus.BAD_REQUEST
body = {
"transaction_id": uuid.uuid4().hex,
"result_code": ResultCodes.INVALID_ACCEPT_HEADER.value,
}
self.response_text = json_dump(body=body)
date = email.utils.formatdate(
timeval=None,
localtime=False,
usegmt=True,
)
self.headers = {
"Connection": "keep-alive",
"Content-Type": "application/json",
"server": "envoy",
"Date": date,
"x-envoy-upstream-service-time": "5",
"Content-Length": str(object=len(self.response_text)),
"strict-transport-security": "max-age=31536000",
"x-aws-region": "us-east-2, us-west-2",
"x-content-type-options": "nosniff",
}


@beartype
class InvalidInstanceIdError(ValidatorError):
"""Exception raised when an invalid instance_id is given."""

def __init__(self) -> None:
"""
Attributes:
status_code: The status code to use in a response if this is
raised.
response_text: The response text to use in a response if this
is
raised.
"""
super().__init__()
self.status_code = HTTPStatus.UNPROCESSABLE_ENTITY
body = {
"transaction_id": uuid.uuid4().hex,
"result_code": ResultCodes.INVALID_INSTANCE_ID.value,
}
self.response_text = json_dump(body=body)
date = email.utils.formatdate(
timeval=None,
localtime=False,
usegmt=True,
)
self.headers = {
"Connection": "keep-alive",
"Content-Type": "application/json",
"server": "envoy",
"Date": date,
"x-envoy-upstream-service-time": "5",
"Content-Length": str(object=len(self.response_text)),
"strict-transport-security": "max-age=31536000",
"x-aws-region": "us-east-2, us-west-2",
"x-content-type-options": "nosniff",
}


@beartype
class TargetStatusProcessingError(ValidatorError):
"""Exception raised when trying to delete a target which is processing."""
Expand Down
Loading
Loading