Skip to content
Open
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
9 changes: 8 additions & 1 deletion server/app/interfaces/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 8 additions & 5 deletions server/app/interfaces/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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", [])
Expand Down
33 changes: 33 additions & 0 deletions server/test/interfaces/test_discovery_asset_ids.py
Original file line number Diff line number Diff line change
@@ -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)
49 changes: 49 additions & 0 deletions server/test/interfaces/test_submodel_list_constraint.py
Original file line number Diff line number Diff line change
@@ -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)