diff --git a/server/app/interfaces/discovery.py b/server/app/interfaces/discovery.py index 47e20205..2646ccd3 100644 --- a/server/app/interfaces/discovery.py +++ b/server/app/interfaces/discovery.py @@ -44,17 +44,18 @@ def get_all_specific_asset_ids_by_aas_id(self, aas_id: model.Identifier) -> List return list(self.aas_id_to_asset_ids.get(aas_id, set())) def add_specific_asset_ids_to_aas(self, aas_id: model.Identifier, asset_ids: List[model.SpecificAssetId]) -> None: - - if aas_id not in self.aas_id_to_asset_ids: - self.aas_id_to_asset_ids[aas_id] = set() - - for asset in asset_ids: - self.aas_id_to_asset_ids[aas_id].add(asset) + self.aas_id_to_asset_ids.setdefault(aas_id, set()).update(asset_ids) + for asset_id in asset_ids: + self.asset_id_to_aas_ids.setdefault(asset_id, set()).add(aas_id) def delete_specific_asset_ids_by_aas_id(self, aas_id: model.Identifier) -> None: - key = aas_id - if key in self.aas_id_to_asset_ids: - del self.aas_id_to_asset_ids[key] + for asset_id in self.aas_id_to_asset_ids.pop(aas_id, set()): + aas_ids = self.asset_id_to_aas_ids.get(asset_id) + if aas_ids is None: + continue + aas_ids.discard(aas_id) + if not aas_ids: + del self.asset_id_to_aas_ids[asset_id] def search_aas_ids_by_asset_link(self, asset_link: server_model.AssetLink) -> List[model.Identifier]: result = [] @@ -63,16 +64,6 @@ def search_aas_ids_by_asset_link(self, asset_link: server_model.AssetLink) -> Li result.extend(list(aas_ids)) return result - def _add_aas_id_to_specific_asset_id(self, asset_id: model.SpecificAssetId, aas_id: model.Identifier) -> None: - if asset_id in self.asset_id_to_aas_ids: - self.asset_id_to_aas_ids[asset_id].add(aas_id) - else: - self.asset_id_to_aas_ids[asset_id] = {aas_id} - - def _delete_aas_id_from_specific_asset_ids(self, asset_id: model.SpecificAssetId, aas_id: model.Identifier) -> None: - if asset_id in self.asset_id_to_aas_ids: - self.asset_id_to_aas_ids[asset_id].discard(aas_id) - @classmethod def from_file(cls, filename: str) -> "DiscoveryStore": """ @@ -88,21 +79,13 @@ def from_file(cls, filename: str) -> "DiscoveryStore": discovery_store = DiscoveryStore() for aas_id, asset_ids in data.get("aas_id_to_asset_ids", {}).items(): - parsed_asset_ids = set() - - for asset_id in asset_ids: - if isinstance(asset_id, model.SpecificAssetId): - parsed_asset_id = asset_id - else: - parsed_asset_id = model.SpecificAssetId( - name=asset_id["name"], - value=asset_id["value"], - ) - - parsed_asset_ids.add(parsed_asset_id) - discovery_store._add_aas_id_to_specific_asset_id(parsed_asset_id, aas_id) - - discovery_store.aas_id_to_asset_ids[aas_id] = parsed_asset_ids + parsed_asset_ids = [ + asset_id + if isinstance(asset_id, model.SpecificAssetId) + else model.SpecificAssetId(name=asset_id["name"], value=asset_id["value"]) + for asset_id in asset_ids + ] + discovery_store.add_specific_asset_ids_to_aas(aas_id, parsed_asset_ids) return discovery_store @@ -224,19 +207,18 @@ def post_all_asset_links_by_id( ) -> Response: aas_identifier = str(url_args["aas_id"]) specific_asset_ids = HTTPApiDecoder.request_body_list(request, model.SpecificAssetId, False) + self.persistent_store.delete_specific_asset_ids_by_aas_id(aas_identifier) self.persistent_store.add_specific_asset_ids_to_aas(aas_identifier, specific_asset_ids) - for asset_id in specific_asset_ids: - self.persistent_store._add_aas_id_to_specific_asset_id(asset_id, aas_identifier) updated = {aas_identifier: self.persistent_store.get_all_specific_asset_ids_by_aas_id(aas_identifier)} - return response_t(updated) + return response_t(updated, status=201) def delete_all_asset_links_by_id( self, request: Request, url_args: dict, response_t: Type[APIResponse], **_kwargs ) -> Response: aas_identifier = str(url_args["aas_id"]) + if aas_identifier not in self.persistent_store.aas_id_to_asset_ids: + raise werkzeug.exceptions.NotFound(f"No AssetAdministrationShell with {aas_identifier} found!") self.persistent_store.delete_specific_asset_ids_by_aas_id(aas_identifier) - for key in list(self.persistent_store.asset_id_to_aas_ids.keys()): - self.persistent_store.asset_id_to_aas_ids[key].discard(aas_identifier) return response_t() diff --git a/server/test/docker_integration/test_docker_integration_discovery.py b/server/test/docker_integration/test_docker_integration_discovery.py index fdb168d7..72959eed 100644 --- a/server/test/docker_integration/test_docker_integration_discovery.py +++ b/server/test/docker_integration/test_docker_integration_discovery.py @@ -46,7 +46,11 @@ def tearDown(self) -> None: delete_request = urllib.request.Request( f"{SERVER_BASE_URL}/lookup/shells/{base64url_encode(self.AAS_ID)}", method="DELETE" ) - urllib.request.urlopen(delete_request).close() + try: + urllib.request.urlopen(delete_request).close() + except urllib.error.HTTPError as exc: + if exc.code != 404: + raise # ------------------------------------------------------------------ GET /description @@ -68,7 +72,7 @@ def test_asset_link_roundtrip(self): aas_asset_links_path, data=body, headers={"Content-Type": "application/json"}, method="POST" ) with urllib.request.urlopen(post_request) as response: - self.assertEqual(200, response.status) + self.assertEqual(201, response.status) with urllib.request.urlopen(aas_asset_links_path) as response: self.assertEqual(200, response.status) diff --git a/server/test/interfaces/test_discovery.py b/server/test/interfaces/test_discovery.py index e5e551a1..056d0b48 100644 --- a/server/test/interfaces/test_discovery.py +++ b/server/test/interfaces/test_discovery.py @@ -71,8 +71,6 @@ def setUp(self) -> None: def register(self, aas_id: str, asset_ids: List[Tuple[str, str]]) -> None: assets: list[SpecificAssetId] = [SpecificAssetId(name, value) for name, value in asset_ids] self.store.add_specific_asset_ids_to_aas(aas_id, assets) - for asset in assets: - self.store._add_aas_id_to_specific_asset_id(asset, aas_id) # ====================================================================== /description @@ -233,8 +231,7 @@ def test_post_creates_asset_links(self) -> None: obj=[{"name": "serial", "value": "123"}], ) - # Spec documents 201 for creation; this server responds 200 with the updated mapping. - self.assert_ok(response) + self.assertEqual(201, response.status_code, msg=response.get_data(as_text=True)) body = self.format_client.parse_object(response) self.assertEqual([("serial", "123")], [(a["name"], a["value"]) for a in body[self.AAS_ID]]) self.assertEqual( @@ -249,22 +246,30 @@ def test_post_accepts_single_object_body(self) -> None: f"/lookup/shells/{base64url_encode(self.AAS_ID)}", obj={"name": "serial", "value": "123"} ) - self.assert_ok(response) + self.assertEqual(201, response.status_code, msg=response.get_data(as_text=True)) self.assertEqual( [("serial", "123")], [(a["name"], a["value"]) for a in self.format_client.parse_object(response)[self.AAS_ID]], ) - def test_post_is_additive(self) -> None: + def test_post_replaces_existing_asset_links(self) -> None: + # Spec: PostAllAssetLinksById "Creates or replaces all asset links associated to the + # Asset Administration Shell". Prior links must be dropped, not merged. self.register(self.AAS_ID, [("serial", "123")]) response = self.format_client.post( f"/lookup/shells/{base64url_encode(self.AAS_ID)}", obj=[{"name": "batch", "value": "xyz"}] ) - self.assert_ok(response) + self.assertEqual(201, response.status_code, msg=response.get_data(as_text=True)) stored = {(a["name"], a["value"]) for a in self.format_client.parse_object(response)[self.AAS_ID]} - self.assertEqual({("serial", "123"), ("batch", "xyz")}, stored) + self.assertEqual({("batch", "xyz")}, stored) + self.assertEqual( + [], + self.format_client.parse_collection( + self.format_client.post("/lookup/shellsByAssetLink", obj=[{"name": "serial", "value": "123"}]) + ), + ) def test_post_malformed_json_returns_400(self) -> None: response = self.format_client.post( @@ -292,11 +297,8 @@ def test_delete_removes_asset_links(self) -> None: ), ) - def test_delete_unknown_aas_returns_204(self) -> None: - # Spec allows 404 here; this server always returns 204. - response = self.format_client.delete(f"/lookup/shells/{base64url_encode(self.UNKNOWN_ID)}") - - self.assertEqual(204, response.status_code, msg=response.get_data(as_text=True)) + def test_delete_unknown_aas_returns_404(self) -> None: + self.assert_error(self.format_client.delete(f"/lookup/shells/{base64url_encode(self.UNKNOWN_ID)}"), 404) # ====================================================================== DiscoveryStore.from_file()/to_file()