diff --git a/server/app/interfaces/discovery.py b/server/app/interfaces/discovery.py index 47e202053..c9a85d97e 100644 --- a/server/app/interfaces/discovery.py +++ b/server/app/interfaces/discovery.py @@ -194,7 +194,14 @@ def get_all_aas_ids_by_asset_link( if not isinstance(item, dict): raise werkzeug.exceptions.BadRequest("Each asset link must be a JSON object") - asset_link = server_model.AssetLink(item["name"], item["value"]) + try: + asset_link = server_model.AssetLink(item["name"], item["value"]) + except KeyError as e: + # Match the shells filter path (#511): missing name/value is a + # client error, not an unhandled KeyError → 500 (#672). + raise werkzeug.exceptions.BadRequest( + f"Invalid assetId format: missing field {e}" + ) from e aas_keys = self.persistent_store.search_aas_ids_by_asset_link(asset_link) matching_aas_keys.update(aas_keys) diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py index fc7b4b0a0..ff964c705 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -792,11 +792,14 @@ def post_submodel_submodel_elements_id_short_path( try: parent.add_referable(new_submodel_element) except model.AASConstraintViolation as e: - if e.constraint_id != 22: - raise - raise Conflict( - f"SubmodelElement with idShort {new_submodel_element.id_short} already exists within {parent}!" - ) + # Constraint 22 is "idShort already taken" → 409; every other + # constraint (e.g. AASd-109 type mismatch in a SubmodelElementList) + # is invalid client input → 400 (#672). + if e.constraint_id == 22: + raise Conflict( + f"SubmodelElement with idShort {new_submodel_element.id_short} already exists within {parent}!" + ) + raise BadRequest(str(e)) from e self.object_store.commit(self._get_submodel(url_args)) submodel = self._get_submodel(url_args) id_short_path = url_args.get("id_shorts", []) diff --git a/server/test/interfaces/test_discovery_asset_ids.py b/server/test/interfaces/test_discovery_asset_ids.py new file mode 100644 index 000000000..26cbae664 --- /dev/null +++ b/server/test/interfaces/test_discovery_asset_ids.py @@ -0,0 +1,33 @@ +# Copyright (c) 2026 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. +# +# SPDX-License-Identifier: MIT + +"""Client-error mapping for discovery ``GET /lookup/shells?assetIds=...`` (#672).""" + +import base64 +import json +import unittest + +from app.interfaces.discovery import DiscoveryAPI, DiscoveryStore +from werkzeug.test import Client + + +def _b64url_json(obj) -> str: + raw = json.dumps(obj, separators=(",", ":")).encode() + return base64.urlsafe_b64encode(raw).decode().rstrip("=") + + +class DiscoveryAssetIdsClientErrorsTest(unittest.TestCase): + def setUp(self) -> None: + self.client = Client(DiscoveryAPI(DiscoveryStore(), base_path="")) + + def test_asset_ids_missing_value_returns_400(self) -> None: + response = self.client.get(f"/lookup/shells?assetIds={_b64url_json({'name': 'serial'})}") + self.assertEqual(400, response.status_code) + + def test_asset_ids_missing_name_returns_400(self) -> None: + response = self.client.get(f"/lookup/shells?assetIds={_b64url_json({'value': '123'})}") + self.assertEqual(400, response.status_code) diff --git a/server/test/interfaces/test_submodel_list_constraint.py b/server/test/interfaces/test_submodel_list_constraint.py new file mode 100644 index 000000000..5635da4c5 --- /dev/null +++ b/server/test/interfaces/test_submodel_list_constraint.py @@ -0,0 +1,49 @@ +# Copyright (c) 2026 the Eclipse BaSyx Authors +# +# This program and the accompanying materials are made available under the terms of the MIT License, available in +# the LICENSE file of this project. +# +# SPDX-License-Identifier: MIT + +"""SubmodelElementList constraint violations must be 400, not 500 (#672).""" + +import json +import unittest +from unittest import mock + +from app.interfaces import repository +from basyx.aas import model +from basyx.aas.adapter import aasx +from basyx.aas.adapter.json import json_serialization +from werkzeug.test import Client + + +class SubmodelListConstraintClientErrorTest(unittest.TestCase): + def setUp(self) -> None: + self.object_store = model.DictIdentifiableStore() + self.file_store = mock.Mock(spec=aasx.AbstractSupplementaryFileContainer) + app = repository.WSGIApp(self.object_store, self.file_store, base_path="") + self.client = Client(app) + + def test_post_property_with_wrong_value_type_returns_400(self) -> None: + submodel = model.Submodel("https://acplt.org/Test_Submodel_ListConstraint") + sml = model.SubmodelElementList( + "L", + model.Property, + value_type_list_element=model.datatypes.String, + ) + submodel.add_referable(sml) + self.object_store.add(submodel) + + payload = json.dumps( + model.Property(None, model.datatypes.Int, 1), + cls=json_serialization.AASToJsonEncoder, + ) + from app.util.converters import base64url_encode + + response = self.client.post( + f"/submodels/{base64url_encode(submodel.id)}/submodel-elements/L", + data=payload, + content_type="application/json", + ) + self.assertEqual(400, response.status_code)