Skip to content

Commit 080b86c

Browse files
Add comprehensive VuMark instance generation tests (#2954)
* Add comprehensive VuMark instance generation tests and mock support - Add tests for PNG, SVG, and PDF output formats (parametrized) - Add tests for invalid Accept header (returns InvalidAcceptHeader 400) - Add tests for empty instance_id (returns InvalidInstanceId 422) - Organise tests into a class with verify_mock_vuforia on the class - Implement Accept header validation and multi-format responses in mock - Add InvalidAcceptHeader and InvalidInstanceId result codes and exceptions - Add minimal SVG and PDF mock response content - Document VuMark image simplification in differences-to-vws.rst Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix mypy errors in VuMark tests and Flask handler - Use argnames=/argvalues= keyword arguments in pytest.mark.parametrize - Use keyword argument key= for Flask Headers.get() call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add svg and pdf to spelling private dictionary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Retry delete_target if TargetStatusProcessingError after update_target Vuforia has a race condition where delete_target can raise TargetStatusProcessingError immediately after wait_for_target_processed returns, because update_target(active_flag=False) triggers a brief reprocessing cycle that may not have started by the time the wait completes. Retry once with another wait if this occurs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Complete remaining todos: ResultCodes constants, spelling revert, retry - Use ResultCodes.INVALID_ACCEPT_HEADER.value and ResultCodes.INVALID_INSTANCE_ID.value in test assertions instead of raw strings - Reword docstrings/docs to avoid acronyms that trigger the spell checker, and revert spelling_private_dict.txt to its pre-PR state - Replace one-off try/except retry in _delete_all_targets with a proper tenacity-based _delete_target_when_processed helper function Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Extract multipart parsing helper; revert retry changes to separate issue - Extract repeated multipart parsing logic in image_validators.py into _parse_multipart_files helper, eliminating four copies of the same six-line pattern - Revert _delete_target_when_processed retry changes from vuforia_backends.py; tracked separately as #2955 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Simplify valid_accept_types: content type equals accept key The dict values were (bytes, str) tuples where the str was always identical to the dict key (the Accept MIME type). Use a plain dict[str, bytes] and derive the content type directly from `accept`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Remove duplicate _parse_multipart_files introduced by merge Both branches independently added the same helper; keep the @beartype-decorated version from main. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix Docker build on PRs; remove duplicate rst section - Pass files: docker-bake.hcl explicitly to docker/bake-action so it uses the local checked-out file rather than fetching from the PR merge ref (refs/pull/*/merge) via HTTPS, which requires auth not available to the bake action - Remove duplicate 'VuMark instance images' section in differences-to-vws.rst introduced by the merge Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Revert docker-build.yml change; Docker failure was transient The Docker build failures are intermittent infrastructure issues, not a systematic problem. The files: docker-bake.hcl change was unnecessary. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7bf40c4 commit 080b86c

File tree

5 files changed

+268
-46
lines changed

5 files changed

+268
-46
lines changed

src/mock_vws/_constants.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@
1111
b"\xaeB`\x82"
1212
)
1313

14+
VUMARK_SVG = (
15+
b'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>'
16+
)
17+
18+
VUMARK_PDF = (
19+
b"%PDF-1.4\n"
20+
b"1 0 obj<</Type /Catalog /Pages 2 0 R>>endobj\n"
21+
b"2 0 obj<</Type /Pages /Kids [3 0 R] /Count 1>>endobj\n"
22+
b"3 0 obj<</Type /Page /MediaBox [0 0 100 100]>>endobj\n"
23+
b"xref\n0 4\n"
24+
b"0000000000 65535 f \n"
25+
b"trailer<</Size 4/Root 1 0 R>>\n"
26+
b"startxref\n9\n%%EOF"
27+
)
28+
1429

1530
@beartype
1631
@unique
@@ -45,6 +60,8 @@ class ResultCodes(Enum):
4560
PROJECT_INACTIVE = "ProjectInactive"
4661
INACTIVE_PROJECT = "InactiveProject"
4762
TOO_MANY_REQUESTS = "TooManyRequests"
63+
INVALID_ACCEPT_HEADER = "InvalidAcceptHeader"
64+
INVALID_INSTANCE_ID = "InvalidInstanceId"
4865

4966

5067
@beartype

src/mock_vws/_flask_server/vws.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@
1818
from flask import Flask, Response, request
1919
from pydantic_settings import BaseSettings
2020

21-
from mock_vws._constants import VUMARK_PNG, ResultCodes, TargetStatuses
21+
from mock_vws._constants import (
22+
VUMARK_PDF,
23+
VUMARK_PNG,
24+
VUMARK_SVG,
25+
ResultCodes,
26+
TargetStatuses,
27+
)
2228
from mock_vws._database_matchers import get_database_matching_server_keys
2329
from mock_vws._mock_common import json_dump
2430
from mock_vws._services_validators import run_services_validators
2531
from mock_vws._services_validators.exceptions import (
2632
FailError,
33+
InvalidAcceptHeaderError,
34+
InvalidInstanceIdError,
2735
TargetStatusNotSuccessError,
2836
TargetStatusProcessingError,
2937
ValidatorError,
@@ -351,10 +359,27 @@ def generate_vumark_instance(target_id: str) -> Response:
351359
"""
352360
# ``target_id`` is validated by request validators.
353361
del target_id
362+
363+
accept = request.headers.get(key="Accept", default="")
364+
valid_accept_types: dict[str, bytes] = {
365+
"image/png": VUMARK_PNG,
366+
"image/svg+xml": VUMARK_SVG,
367+
"application/pdf": VUMARK_PDF,
368+
}
369+
if accept not in valid_accept_types:
370+
raise InvalidAcceptHeaderError
371+
372+
request_json = json.loads(s=request.data)
373+
instance_id = request_json.get("instance_id", "")
374+
if not instance_id:
375+
raise InvalidInstanceIdError
376+
377+
response_body = valid_accept_types[accept]
378+
content_type = accept
354379
date = email.utils.formatdate(timeval=None, localtime=False, usegmt=True)
355380
headers = {
356381
"Connection": "keep-alive",
357-
"Content-Type": "image/png",
382+
"Content-Type": content_type,
358383
"server": "envoy",
359384
"Date": date,
360385
"x-envoy-upstream-service-time": "5",
@@ -364,7 +389,7 @@ def generate_vumark_instance(target_id: str) -> Response:
364389
}
365390
return Response(
366391
status=HTTPStatus.OK,
367-
response=VUMARK_PNG,
392+
response=response_body,
368393
headers=headers,
369394
)
370395

src/mock_vws/_requests_mock_server/mock_web_services_api.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@
1818
from beartype import BeartypeConf, beartype
1919
from requests.models import PreparedRequest
2020

21-
from mock_vws._constants import VUMARK_PNG, ResultCodes, TargetStatuses
21+
from mock_vws._constants import (
22+
VUMARK_PDF,
23+
VUMARK_PNG,
24+
VUMARK_SVG,
25+
ResultCodes,
26+
TargetStatuses,
27+
)
2228
from mock_vws._database_matchers import get_database_matching_server_keys
2329
from mock_vws._mock_common import Route, json_dump
2430
from mock_vws._services_validators import run_services_validators
2531
from mock_vws._services_validators.exceptions import (
2632
FailError,
33+
InvalidAcceptHeaderError,
34+
InvalidInstanceIdError,
2735
TargetStatusNotSuccessError,
2836
TargetStatusProcessingError,
2937
ValidatorError,
@@ -295,30 +303,49 @@ def generate_vumark_instance(
295303
self, request: PreparedRequest
296304
) -> _ResponseType:
297305
"""Generate a VuMark instance."""
298-
run_services_validators(
299-
request_headers=request.headers,
300-
request_body=_body_bytes(request=request),
301-
request_method=request.method or "",
302-
request_path=request.path_url,
303-
databases=self._target_manager.databases,
304-
)
306+
valid_accept_types: dict[str, bytes] = {
307+
"image/png": VUMARK_PNG,
308+
"image/svg+xml": VUMARK_SVG,
309+
"application/pdf": VUMARK_PDF,
310+
}
311+
try:
312+
run_services_validators(
313+
request_headers=request.headers,
314+
request_body=_body_bytes(request=request),
315+
request_method=request.method or "",
316+
request_path=request.path_url,
317+
databases=self._target_manager.databases,
318+
)
319+
320+
accept = dict(request.headers).get("Accept", "")
321+
if accept not in valid_accept_types:
322+
raise InvalidAcceptHeaderError
323+
324+
request_json = json.loads(s=_body_bytes(request=request))
325+
instance_id = request_json.get("instance_id", "")
326+
if not instance_id:
327+
raise InvalidInstanceIdError
328+
except ValidatorError as exc:
329+
return exc.status_code, exc.headers, exc.response_text
305330

331+
response_body = valid_accept_types[accept]
332+
content_type = accept
306333
date = email.utils.formatdate(
307334
timeval=None,
308335
localtime=False,
309336
usegmt=True,
310337
)
311338
headers = {
312339
"Connection": "keep-alive",
313-
"Content-Type": "image/png",
340+
"Content-Type": content_type,
314341
"Date": date,
315342
"server": "envoy",
316343
"x-envoy-upstream-service-time": "5",
317344
"strict-transport-security": "max-age=31536000",
318345
"x-aws-region": "us-east-2, us-west-2",
319346
"x-content-type-options": "nosniff",
320347
}
321-
return HTTPStatus.OK, headers, VUMARK_PNG
348+
return HTTPStatus.OK, headers, response_body
322349

323350
@route(path_pattern="/summary", http_methods={HTTPMethod.GET})
324351
def database_summary(self, request: PreparedRequest) -> _ResponseType:

src/mock_vws/_services_validators/exceptions.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,82 @@ def __init__(self) -> None:
530530
}
531531

532532

533+
@beartype
534+
class InvalidAcceptHeaderError(ValidatorError):
535+
"""Exception raised when an unsupported Accept header is given."""
536+
537+
def __init__(self) -> None:
538+
"""
539+
Attributes:
540+
status_code: The status code to use in a response if this is
541+
raised.
542+
response_text: The response text to use in a response if this
543+
is
544+
raised.
545+
"""
546+
super().__init__()
547+
self.status_code = HTTPStatus.BAD_REQUEST
548+
body = {
549+
"transaction_id": uuid.uuid4().hex,
550+
"result_code": ResultCodes.INVALID_ACCEPT_HEADER.value,
551+
}
552+
self.response_text = json_dump(body=body)
553+
date = email.utils.formatdate(
554+
timeval=None,
555+
localtime=False,
556+
usegmt=True,
557+
)
558+
self.headers = {
559+
"Connection": "keep-alive",
560+
"Content-Type": "application/json",
561+
"server": "envoy",
562+
"Date": date,
563+
"x-envoy-upstream-service-time": "5",
564+
"Content-Length": str(object=len(self.response_text)),
565+
"strict-transport-security": "max-age=31536000",
566+
"x-aws-region": "us-east-2, us-west-2",
567+
"x-content-type-options": "nosniff",
568+
}
569+
570+
571+
@beartype
572+
class InvalidInstanceIdError(ValidatorError):
573+
"""Exception raised when an invalid instance_id is given."""
574+
575+
def __init__(self) -> None:
576+
"""
577+
Attributes:
578+
status_code: The status code to use in a response if this is
579+
raised.
580+
response_text: The response text to use in a response if this
581+
is
582+
raised.
583+
"""
584+
super().__init__()
585+
self.status_code = HTTPStatus.UNPROCESSABLE_ENTITY
586+
body = {
587+
"transaction_id": uuid.uuid4().hex,
588+
"result_code": ResultCodes.INVALID_INSTANCE_ID.value,
589+
}
590+
self.response_text = json_dump(body=body)
591+
date = email.utils.formatdate(
592+
timeval=None,
593+
localtime=False,
594+
usegmt=True,
595+
)
596+
self.headers = {
597+
"Connection": "keep-alive",
598+
"Content-Type": "application/json",
599+
"server": "envoy",
600+
"Date": date,
601+
"x-envoy-upstream-service-time": "5",
602+
"Content-Length": str(object=len(self.response_text)),
603+
"strict-transport-security": "max-age=31536000",
604+
"x-aws-region": "us-east-2, us-west-2",
605+
"x-content-type-options": "nosniff",
606+
}
607+
608+
533609
@beartype
534610
class TargetStatusProcessingError(ValidatorError):
535611
"""Exception raised when trying to delete a target which is processing."""

0 commit comments

Comments
 (0)