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
13 changes: 10 additions & 3 deletions docs/source/differences-to-vws.rst
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,17 @@ Dataset creation requests are validated for the required top-level ``models``,
entry being a JSON object, and for the number of models.
Each model is validated for the required ``name`` field, for exactly one of
``cadDataUrl`` and ``cadDataBlob`` being given, for the types of the
``cadDataBlob``, ``cadDataFormat``, ``cadDataUrl`` and ``name`` fields, for
``cadDataFormat`` being one of ``DAE``, ``FBX``, ``GLB``, ``IGES``, ``OBJ``,
``PVZ``, ``STL``, ``VRML`` and ``ZIP`` when it is given, and for ``views``
``automaticColoring``, ``cadDataBlob``, ``cadDataFormat``, ``cadDataUrl``,
``motionHint``, ``name``, ``optimizeTrackingFor``, ``simplify`` and
``trackingMode`` fields, for each of the ``automaticColoring``,
``cadDataFormat``, ``motionHint``, ``optimizeTrackingFor``, ``simplify`` and
``trackingMode`` fields being one of the values which the Model Target OpenAPI
specification documents for it when the field is given, and for ``views``
being a JSON array when it is given.
The ``realisticAppearance`` model field is validated in the same way for
advanced datasets; the OpenAPI specification does not document it as a
standard dataset model field, so standard dataset creation does not validate
it.
Each ``views`` entry is validated for being a JSON object, for the required
``guideViewPosition`` and ``name`` fields, and for those fields' types.
Each ``guideViewPosition`` object is validated for the required ``rotation``
Expand Down
1 change: 1 addition & 0 deletions newsfragments/model-target-enum-fields.change
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reject Model Target dataset creation requests with values outside the documented enumerations for the ``automaticColoring``, ``motionHint``, ``optimizeTrackingFor``, ``realisticAppearance``, ``simplify`` and ``trackingMode`` model fields.
75 changes: 50 additions & 25 deletions src/mock_vws/_model_target_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,36 @@ def remove_model_target_dataset(self, dataset_uuid: str) -> None:
# ``userId:7635391``. The numeric portion is per-account in real Vuforia;
# the mock uses a fixed placeholder.
_MOCK_USER_TARGET = "userId:mock"
# The CAD data formats documented by the Model Target Web API OpenAPI
# specification.
_CAD_DATA_FORMATS = frozenset(
{
"DAE",
"FBX",
"GLB",
"IGES",
"OBJ",
"PVZ",
"STL",
"VRML",
"ZIP",
},
)
# The enumerated model field values documented by the Model Target Web API
# OpenAPI specification.
_MODEL_ENUM_FIELD_VALUES: dict[str, frozenset[str]] = {
"automaticColoring": frozenset({"always", "auto", "never"}),
"cadDataFormat": frozenset(
{
"DAE",
"FBX",
"GLB",
"IGES",
"OBJ",
"PVZ",
"STL",
"VRML",
"ZIP",
},
),
"motionHint": frozenset({"adaptive", "dynamic", "static"}),
"optimizeTrackingFor": frozenset(
{"ar_controller", "default", "low_feature_objects"},
),
"simplify": frozenset({"always", "auto", "never"}),
"trackingMode": frozenset({"car", "default", "scan"}),
}
# ``realisticAppearance`` is documented as an enumerated model field for
# advanced datasets only.
_ADVANCED_MODEL_ENUM_FIELD_VALUES: dict[str, frozenset[str]] = {
**_MODEL_ENUM_FIELD_VALUES,
"realisticAppearance": frozenset({"auto", "false", "true"}),
}


@beartype
Expand Down Expand Up @@ -422,8 +437,17 @@ def _cad_data_source_details(*, models: list[Any]) -> list[dict[str, str]]:


@beartype
def _model_field_details(*, models: list[Any]) -> list[dict[str, str]]:
def _model_field_details(
*,
models: list[Any],
dataset_type: ModelTargetDatasetType,
) -> list[dict[str, str]]:
"""Return validation details for the fields of each model."""
enum_field_values = (
_ADVANCED_MODEL_ENUM_FIELD_VALUES
if dataset_type == ModelTargetDatasetType.ADVANCED
else _MODEL_ENUM_FIELD_VALUES
)
missing_details = [
{
"code": "VALIDATION_ERROR",
Expand All @@ -436,28 +460,29 @@ def _model_field_details(*, models: list[Any]) -> list[dict[str, str]]:
if missing_details or cad_data_source_details:
return missing_details + cad_data_source_details

string_fields = sorted(
{"cadDataBlob", "cadDataUrl", "name", *enum_field_values},
)
string_details = [
{
"code": "VALIDATION_ERROR",
"message": f"/models({index})/{field}: error.expected.jsstring",
}
for index, model in enumerate(iterable=models)
for field in ("cadDataBlob", "cadDataFormat", "cadDataUrl", "name")
for field in string_fields
if field in model and not isinstance(model[field], str)
]
if string_details:
return string_details

format_details = [
enum_details = [
{
"code": "VALIDATION_ERROR",
"message": (
f"/models({index})/cadDataFormat: error.expected.validenum"
),
"message": f"/models({index})/{field}: error.expected.validenum",
}
for index, model in enumerate(iterable=models)
if "cadDataFormat" in model
and model["cadDataFormat"] not in _CAD_DATA_FORMATS
for field, allowed_values in sorted(enum_field_values.items())
if field in model and model[field] not in allowed_values
]
views_details = [
{
Expand All @@ -467,7 +492,7 @@ def _model_field_details(*, models: list[Any]) -> list[dict[str, str]]:
for index, model in enumerate(iterable=models)
if "views" in model and not isinstance(model["views"], list)
]
return format_details + views_details
return enum_details + views_details


@beartype
Expand Down Expand Up @@ -694,7 +719,7 @@ def _validate_dataset_request(
if not details:
models: list[Any] = [*request_json["models"]]
details = (
_model_field_details(models=models)
_model_field_details(models=models, dataset_type=dataset_type)
or _view_details(models=models)
or _guide_view_position_details(models=models)
or _model_count_details(
Expand Down
106 changes: 106 additions & 0 deletions tests/mock_vws/test_model_target_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,71 @@ def test_invalid_basic_auth_header(*, authorization: str) -> None:
{"/models(0)/name: error.expected.jsstring"},
id="model-name-not-string",
),
pytest.param(
{
**_UNAUTHENTICATED_DATASET_REQUEST,
"models": [{**_MODEL, "simplify": 1}],
},
{"/models(0)/simplify: error.expected.jsstring"},
id="model-simplify-not-string",
),
pytest.param(
{
**_UNAUTHENTICATED_DATASET_REQUEST,
"models": [{**_MODEL, "simplify": "sometimes"}],
},
{"/models(0)/simplify: error.expected.validenum"},
id="model-simplify-not-in-enum",
),
pytest.param(
{
**_UNAUTHENTICATED_DATASET_REQUEST,
"models": [{**_MODEL, "automaticColoring": "sometimes"}],
},
{"/models(0)/automaticColoring: error.expected.validenum"},
id="model-automatic-coloring-not-in-enum",
),
pytest.param(
{
**_UNAUTHENTICATED_DATASET_REQUEST,
"models": [{**_MODEL, "motionHint": "still"}],
},
{"/models(0)/motionHint: error.expected.validenum"},
id="model-motion-hint-not-in-enum",
),
pytest.param(
{
**_UNAUTHENTICATED_DATASET_REQUEST,
"models": [{**_MODEL, "optimizeTrackingFor": "cars"}],
},
{"/models(0)/optimizeTrackingFor: error.expected.validenum"},
id="model-optimize-tracking-for-not-in-enum",
),
pytest.param(
{
**_UNAUTHENTICATED_DATASET_REQUEST,
"models": [{**_MODEL, "trackingMode": "boat"}],
},
{"/models(0)/trackingMode: error.expected.validenum"},
id="model-tracking-mode-not-in-enum",
),
pytest.param(
{
**_UNAUTHENTICATED_DATASET_REQUEST,
"models": [
{
**_MODEL,
"motionHint": "still",
"simplify": "sometimes",
},
],
},
{
"/models(0)/motionHint: error.expected.validenum",
"/models(0)/simplify: error.expected.validenum",
},
id="model-multiple-enum-errors",
),
pytest.param(
{
**_UNAUTHENTICATED_DATASET_REQUEST,
Expand Down Expand Up @@ -998,6 +1063,47 @@ def test_advanced_model_count_exceeds_limit() -> None:
assert error["code"] == "BAD_REQUEST"
assert error["details"][0]["code"] == "VALIDATION_ERROR"

@staticmethod
def test_advanced_realistic_appearance_not_in_enum() -> None:
"""Advanced dataset requests with a ``realisticAppearance`` value
outside the documented enumeration are rejected.

The Model Target OpenAPI specification documents
``realisticAppearance`` as a model field for advanced datasets
only, so standard dataset creation does not validate it. This is
mock-only because the available test account lacks the
advanced-dataset scope, so real Vuforia rejects the request with a
403 before validating the body.
"""
body = {
**_UNAUTHENTICATED_DATASET_REQUEST,
"models": [{**_MODEL, "realisticAppearance": "yes"}],
}
headers = {"Authorization": f"Bearer {_MOCK_BEARER_TOKEN}"}
with MockVWS():
advanced_response = requests.post(
url=f"{_VWS_HOST}/modeltargets/advancedDatasets",
headers=headers,
json=body,
timeout=30,
)
standard_response = requests.post(
url=f"{_VWS_HOST}/modeltargets/datasets",
headers=headers,
json=body,
timeout=30,
)

assert advanced_response.status_code == HTTPStatus.BAD_REQUEST
error = advanced_response.json()["error"]
assert error["code"] == "BAD_REQUEST"
assert [detail["message"] for detail in error["details"]] == [
"/models(0)/realisticAppearance: error.expected.validenum",
]
assert error["details"][0]["code"] == "VALIDATION_ERROR"

assert standard_response.status_code == HTTPStatus.CREATED

@staticmethod
def test_processing_dataset_cannot_be_downloaded() -> None:
"""A dataset cannot be downloaded while it is still processing.
Expand Down
Loading