From 5706951d4525d98033b30bd16698ad91127619e7 Mon Sep 17 00:00:00 2001
From: ewuerger <ernst.wuerger@gmail.com>
Date: Tue, 20 Aug 2024 10:41:49 +0200
Subject: [PATCH 1/6] fix: Remove obsolete link serializers

---
 capella2polarion/converters/link_converter.py | 25 +++----------------
 1 file changed, 3 insertions(+), 22 deletions(-)

diff --git a/capella2polarion/converters/link_converter.py b/capella2polarion/converters/link_converter.py
index 7a16dddc..2b9141a8 100644
--- a/capella2polarion/converters/link_converter.py
+++ b/capella2polarion/converters/link_converter.py
@@ -23,7 +23,7 @@
 
 TYPE_RESOLVERS = {"Part": lambda obj: obj.type.uuid}
 _Serializer: t.TypeAlias = cabc.Callable[
-    [common.GenericElement, str, str, str, dict[str, t.Any]],
+    [common.GenericElement, str, str, dict[str, t.Any]],
     list[polarion_api.WorkItemLink],
 ]
 
@@ -48,8 +48,6 @@ def __init__(
         self.serializers: dict[str, _Serializer] = {
             converter_config.DESCRIPTION_REFERENCE_SERIALIZER: self._handle_description_reference_links,  # pylint: disable=line-too-long
             converter_config.DIAGRAM_ELEMENTS_SERIALIZER: self._handle_diagram_reference_links,  # pylint: disable=line-too-long
-            "input_exchanges": self._handle_exchanges,
-            "output_exchanges": self._handle_exchanges,
         }
 
     def create_links_for_work_item(
@@ -60,6 +58,7 @@ def create_links_for_work_item(
         obj = converter_data.capella_element
         work_item = converter_data.work_item
         assert work_item is not None
+        assert work_item.id is not None
         new_links: list[polarion_api.WorkItemLink] = []
         link_errors: list[str] = []
         for link_config in converter_data.type_config.links:
@@ -71,7 +70,7 @@ def create_links_for_work_item(
             try:
                 if serializer:
                     new_links.extend(
-                        serializer(obj, work_item.id, role_id, attr_id, {})
+                        serializer(obj, work_item.id, role_id, {})
                     )
                 else:
                     refs = _resolve_attribute(obj, attr_id)
@@ -137,10 +136,8 @@ def _handle_description_reference_links(
         obj: common.GenericElement,
         work_item_id: str,
         role_id: str,
-        attr_id: str,
         links: dict[str, polarion_api.WorkItemLink],
     ) -> list[polarion_api.WorkItemLink]:
-        del attr_id
         refs = self.converter_session[obj.uuid].description_references
         ref_set = set(self._get_work_item_ids(work_item_id, refs, role_id))
         return self._create(work_item_id, role_id, ref_set, links)
@@ -150,10 +147,8 @@ def _handle_diagram_reference_links(
         obj: diag.Diagram,
         work_item_id: str,
         role_id: str,
-        attr_id: str,
         links: dict[str, polarion_api.WorkItemLink],
     ) -> list[polarion_api.WorkItemLink]:
-        del attr_id
         try:
             refs = set(self._collect_uuids(obj.nodes))
             refs = set(self._get_work_item_ids(work_item_id, refs, role_id))
@@ -199,20 +194,6 @@ def _create(
         ]
         return list(filter(None, _new_links))
 
-    def _handle_exchanges(
-        self,
-        obj: fa.Function,
-        work_item_id: str,
-        role_id: str,
-        attr_id: str,
-        links: dict[str, polarion_api.WorkItemLink],
-    ) -> list[polarion_api.WorkItemLink]:
-        exchanges: list[str] = []
-        objs = _resolve_attribute(obj, attr_id)
-        exs = self._get_work_item_ids(work_item_id, objs.by_uuid, role_id)
-        exchanges.extend(set(exs))
-        return self._create(work_item_id, role_id, exchanges, links)
-
     def create_grouped_link_fields(
         self,
         data: data_session.ConverterData,

From 318b9597ef8a2163db154fdb5b2071563bcdabf4 Mon Sep 17 00:00:00 2001
From: ewuerger <ernst.wuerger@gmail.com>
Date: Tue, 20 Aug 2024 10:42:02 +0200
Subject: [PATCH 2/6] test: Fix CLI test

---
 tests/test_cli.py      | 32 ++++++++++++++++++++++----------
 tests/test_elements.py |  4 ++--
 2 files changed, 24 insertions(+), 12 deletions(-)

diff --git a/tests/test_cli.py b/tests/test_cli.py
index 2620374d..30e7b79a 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -11,7 +11,8 @@
 from click import testing
 
 import capella2polarion.__main__ as main
-from capella2polarion.connectors.polarion_worker import CapellaPolarionWorker
+from capella2polarion.connectors import polarion_worker
+from capella2polarion.converters import model_converter
 
 # pylint: disable-next=relative-beyond-top-level, useless-suppression
 from .conftest import (  # type: ignore[import]
@@ -26,25 +27,31 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch):
     monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api)
     mock_get_polarion_wi_map = mock.MagicMock()
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "load_polarion_work_item_map",
         mock_get_polarion_wi_map,
     )
+    mock_generate_work_items = mock.MagicMock()
+    monkeypatch.setattr(
+        model_converter.ModelConverter,
+        "generate_work_items",
+        mock_generate_work_items,
+    )
     mock_delete_work_items = mock.MagicMock()
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "delete_orphaned_work_items",
         mock_delete_work_items,
     )
     mock_post_work_items = mock.MagicMock()
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "create_missing_work_items",
         mock_post_work_items,
     )
     mock_patch_work_items = mock.MagicMock()
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "compare_and_update_work_items",
         mock_patch_work_items,
     )
@@ -68,6 +75,11 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch):
 
     assert result.exit_code == 0
     assert mock_get_polarion_wi_map.call_count == 1
+    assert mock_generate_work_items.call_count == 2
+    assert mock_generate_work_items.call_args_list[1][1] == {
+        "generate_links": True,
+        "generate_attachments": True,
+    }
     assert mock_delete_work_items.call_count == 1
     assert mock_patch_work_items.call_count == 1
     assert mock_post_work_items.call_count == 1
@@ -78,7 +90,7 @@ def test_render_documents(monkeypatch: pytest.MonkeyPatch):
     monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api)
     mock_get_polarion_wi_map = mock.MagicMock()
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "load_polarion_work_item_map",
         mock_get_polarion_wi_map,
     )
@@ -96,25 +108,25 @@ def test_render_documents(monkeypatch: pytest.MonkeyPatch):
         else None
     )
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "get_document",
         mock_get_document,
     )
     mock_post_documents = mock.MagicMock()
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "post_documents",
         mock_post_documents,
     )
     mock_update_documents = mock.MagicMock()
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "update_documents",
         mock_update_documents,
     )
     mock_update_work_items = mock.MagicMock()
     monkeypatch.setattr(
-        CapellaPolarionWorker,
+        polarion_worker.CapellaPolarionWorker,
         "update_work_items",
         mock_update_work_items,
     )
diff --git a/tests/test_elements.py b/tests/test_elements.py
index 2c7682ba..ec74f1c4 100644
--- a/tests/test_elements.py
+++ b/tests/test_elements.py
@@ -474,7 +474,7 @@ def error():
             assert False
 
         link_serializer.serializers["invalid_role"] = (
-            lambda obj, work_item_id, role_id, attr_id, links: error()
+            lambda obj, work_item_id, role_id, links: error()
         )
 
         with caplog.at_level(logging.ERROR):
@@ -550,7 +550,7 @@ def error():
             assert False
 
         link_serializer.serializers["invalid_role"] = (
-            lambda obj, work_item_id, role_id, attr_id, links: error()
+            lambda obj, work_item_id, role_id, links: error()
         )
 
         with caplog.at_level(logging.WARNING):

From f60aafa8d88a5bd2d38d26d8c6e639d2c396343f Mon Sep 17 00:00:00 2001
From: ewuerger <ernst.wuerger@gmail.com>
Date: Tue, 20 Aug 2024 13:49:50 +0200
Subject: [PATCH 3/6] ci: Please pylint and mypy

---
 capella2polarion/converters/converter_config.py  | 6 +++---
 capella2polarion/converters/document_renderer.py | 6 ++++--
 tests/test_cli.py                                | 3 ++-
 3 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py
index b037fcc5..9f45d67c 100644
--- a/capella2polarion/converters/converter_config.py
+++ b/capella2polarion/converters/converter_config.py
@@ -37,7 +37,7 @@ class LinkConfig:
     """
 
     capella_attr: str
-    polarion_role: str | None = None
+    polarion_role: str
     include: dict[str, str] = dataclasses.field(default_factory=dict)
 
 
@@ -288,9 +288,9 @@ def _filter_links(
     for link in links:
         cappela_attr = link.capella_attr.split(".")[0]
         if (
-            cappela_attr == DESCRIPTION_REFERENCE_SERIALIZER
+            cappela_attr.endswith(DESCRIPTION_REFERENCE_SERIALIZER)
             or (
-                cappela_attr == DIAGRAM_ELEMENTS_SERIALIZER
+                cappela_attr.endswith(DIAGRAM_ELEMENTS_SERIALIZER)
                 and c_class == diagram.Diagram
             )
             or hasattr(c_class, cappela_attr)
diff --git a/capella2polarion/converters/document_renderer.py b/capella2polarion/converters/document_renderer.py
index 04d3bc3c..7b012754 100644
--- a/capella2polarion/converters/document_renderer.py
+++ b/capella2polarion/converters/document_renderer.py
@@ -433,7 +433,8 @@ def _render_full_authority_documents(
                         )
                     except Exception as e:
                         logger.error(
-                            "Rendering for document %s/%s failed with the following error",
+                            "Rendering for document %s/%s failed with the "
+                            "following error",
                             instance.polarion_space,
                             instance.polarion_name,
                             exc_info=e,
@@ -456,7 +457,8 @@ def _render_full_authority_documents(
                         )
                     except Exception as e:
                         logger.error(
-                            "Rendering for document %s/%s failed with the following error",
+                            "Rendering for document %s/%s failed with the "
+                            "following error",
                             instance.polarion_space,
                             instance.polarion_name,
                             exc_info=e,
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 30e7b79a..6d7516f6 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -101,7 +101,8 @@ def test_render_documents(monkeypatch: pytest.MonkeyPatch):
             module_name=name,
             home_page_content=polarion_api.TextContent(
                 "text/html",
-                '<h1 id="polarion_wiki macro name=module-workitem;params=id=TEST-123></h1>',
+                '<h1 id="polarion_wiki macro name=module-workitem;'
+                'params=id=TEST-123"></h1>',
             ),
         )
         if name == "id1236"

From f39bf54f861f6c1beda2d480ee8b301746570095 Mon Sep 17 00:00:00 2001
From: ewuerger <ernst.wuerger@gmail.com>
Date: Tue, 20 Aug 2024 13:50:13 +0200
Subject: [PATCH 4/6] fix: Role-prefix bug in LinkConverter

---
 capella2polarion/converters/link_converter.py | 23 +++++++++----------
 1 file changed, 11 insertions(+), 12 deletions(-)

diff --git a/capella2polarion/converters/link_converter.py b/capella2polarion/converters/link_converter.py
index 2b9141a8..4e4d46b1 100644
--- a/capella2polarion/converters/link_converter.py
+++ b/capella2polarion/converters/link_converter.py
@@ -12,7 +12,6 @@
 import polarion_rest_api_client as polarion_api
 from capellambse.model import common
 from capellambse.model import diagram as diag
-from capellambse.model.crosslayer import fa
 
 import capella2polarion.converters.polarion_html_helper
 from capella2polarion import data_models
@@ -64,7 +63,7 @@ def create_links_for_work_item(
         for link_config in converter_data.type_config.links:
             assert (role_id := link_config.polarion_role) is not None
             attr_id = link_config.capella_attr or ""
-            serializer = self.serializers.get(role_id)
+            serializer = self.serializers.get(attr_id)
             if self.role_prefix:
                 role_id = f"{self.role_prefix}_{role_id}"
             try:
@@ -228,11 +227,12 @@ def create_grouped_link_fields(
                     config = link_config
                     break
 
+            role_id = role
+            if self.role_prefix:
+                role_id = role.removeprefix(f"{self.role_prefix}_")
+
             self._create_link_fields(
-                work_item,
-                role.removeprefix(f"{self.role_prefix}_"),
-                grouped_links,
-                config=config,
+                work_item, role_id, grouped_links, config=config
             )
 
     def _create_link_fields(
@@ -315,12 +315,11 @@ def create_grouped_back_link_fields(
             List of links referencing work_item as secondary
         """
         for role, grouped_links in _group_by("role", links).items():
-            self._create_link_fields(
-                work_item,
-                role.removeprefix(f"{self.role_prefix}_"),
-                grouped_links,
-                True,
-            )
+            role_id = role
+            if self.role_prefix:
+                role_id = role.removeprefix(f"{self.role_prefix}_")
+
+            self._create_link_fields(work_item, role_id, grouped_links, True)
 
 
 def _group_by(

From a1947768c56011a78a853594a69481b1af0ab079 Mon Sep 17 00:00:00 2001
From: ewuerger <ernst.wuerger@gmail.com>
Date: Tue, 20 Aug 2024 13:50:34 +0200
Subject: [PATCH 5/6] test: Add tests for role-prefix

---
 tests/test_elements.py | 129 +++++++++++++++++++++++++++++++++++++----
 1 file changed, 117 insertions(+), 12 deletions(-)

diff --git a/tests/test_elements.py b/tests/test_elements.py
index ec74f1c4..a594b8b3 100644
--- a/tests/test_elements.py
+++ b/tests/test_elements.py
@@ -1081,7 +1081,11 @@ def test_maintain_grouped_links_attributes(
     ):
         config = converter_config.CapellaTypeConfig(
             "fakeModelObject",
-            links=[converter_config.LinkConfig("attribute")],
+            links=[
+                converter_config.LinkConfig(
+                    capella_attr="attribute", polarion_role="attribute"
+                )
+            ],
         )
         mock_model = mock.MagicMock()
         fake_2 = FakeModelObject("uuid2", "Fale 2")
@@ -1123,6 +1127,48 @@ def test_maintain_grouped_links_attributes(
         assert dummy_work_items["uuid1"].additional_attributes == {}
         assert dummy_work_items["uuid2"].additional_attributes == {}
 
+    @staticmethod
+    def test_maintain_grouped_links_attributes_with_role_prefix(
+        base_object: BaseObjectContainer,
+        dummy_work_items: dict[str, data_models.CapellaWorkItem],
+    ):
+        config = converter_config.CapellaTypeConfig(
+            "fakeModelObject",
+            links=[
+                converter_config.LinkConfig(
+                    capella_attr="attribute", polarion_role="attribute"
+                )
+            ],
+        )
+        mock_model = mock.MagicMock()
+        fake_2 = FakeModelObject("uuid2", "Fale 2")
+        fake_1 = FakeModelObject("uuid1", "Fake 1")
+        fake_0 = FakeModelObject("uuid0", "Fake 0", attribute=[fake_1, fake_2])
+        fake_1.attribute = [fake_0, fake_2]
+        mock_model.by_uuid.side_effect = lambda uuid: {
+            "uuid0": fake_0,
+            "uuid1": fake_1,
+            "uuid2": fake_2,
+        }[uuid]
+        for link in dummy_work_items["uuid0"].linked_work_items:
+            link.role = f"_C2P_{link.role}"
+        link_serializer = link_converter.LinkSerializer(
+            base_object.pw.polarion_data_repo,
+            base_object.mc.converter_session,
+            base_object.pw.polarion_params.project_id,
+            mock_model,
+            role_prefix="_C2P",
+        )
+
+        for work_item in dummy_work_items.values():
+            converter_data = data_session.ConverterData(
+                "test", config, [], work_item
+            )
+            link_serializer.create_grouped_link_fields(converter_data)
+
+        assert "attribute" in dummy_work_items["uuid0"].additional_attributes
+        assert "attribute" in dummy_work_items["uuid1"].additional_attributes
+
     @staticmethod
     def test_grouped_links_attributes_with_includes(
         base_object: BaseObjectContainer, model: capellambse.MelodyModel
@@ -1212,7 +1258,11 @@ def test_maintain_reverse_grouped_links_attributes(
         back_links: dict[str, list[polarion_api.WorkItemLink]] = {}
         config = converter_config.CapellaTypeConfig(
             "fakeModelObject",
-            links=[converter_config.LinkConfig("attribute")],
+            links=[
+                converter_config.LinkConfig(
+                    capella_attr="attribute", polarion_role="attribute"
+                )
+            ],
         )
         mock_model = mock.MagicMock()
         fake_2 = FakeModelObject("uuid2", "Fake 2")
@@ -1268,6 +1318,61 @@ def test_maintain_reverse_grouped_links_attributes(
         assert dummy_work_items["uuid1"].additional_attributes == {}
         assert dummy_work_items["uuid2"].additional_attributes == {}
 
+    @staticmethod
+    def test_maintain_reverse_grouped_links_attributes_with_role_prefix(
+        base_object: BaseObjectContainer,
+        dummy_work_items: dict[str, data_models.CapellaWorkItem],
+    ):
+        reverse_polarion_id_map = {v: k for k, v in POLARION_ID_MAP.items()}
+        back_links: dict[str, list[polarion_api.WorkItemLink]] = {}
+        config = converter_config.CapellaTypeConfig(
+            "fakeModelObject",
+            links=[
+                converter_config.LinkConfig(
+                    capella_attr="attribute", polarion_role="attribute"
+                )
+            ],
+        )
+        mock_model = mock.MagicMock()
+        fake_2 = FakeModelObject("uuid2", "Fake 2")
+        fake_1 = FakeModelObject("uuid1", "Fake 1")
+        fake_0 = FakeModelObject("uuid0", "Fake 0", attribute=[fake_1, fake_2])
+        fake_1.attribute = [fake_0, fake_2]
+        mock_model.by_uuid.side_effect = lambda uuid: {
+            "uuid0": fake_0,
+            "uuid1": fake_1,
+            "uuid2": fake_2,
+        }[uuid]
+        for link in dummy_work_items["uuid0"].linked_work_items:
+            link.role = f"_C2P_{link.role}"
+        link_serializer = link_converter.LinkSerializer(
+            base_object.pw.polarion_data_repo,
+            base_object.mc.converter_session,
+            base_object.pw.polarion_params.project_id,
+            mock_model,
+            role_prefix="_C2P",
+        )
+        for work_item in dummy_work_items.values():
+            converter_data = data_session.ConverterData(
+                "test", config, [], work_item
+            )
+            link_serializer.create_grouped_link_fields(
+                converter_data, back_links
+            )
+
+        for work_item_id, links in back_links.items():
+            work_item = dummy_work_items[reverse_polarion_id_map[work_item_id]]
+            link_serializer.create_grouped_back_link_fields(work_item, links)
+
+        assert (
+            "attribute_reverse"
+            in dummy_work_items["uuid0"].additional_attributes
+        )
+        assert (
+            "attribute_reverse"
+            in dummy_work_items["uuid1"].additional_attributes
+        )
+
 
 def test_grouped_linked_work_items_order_consistency(
     base_object: BaseObjectContainer,
@@ -1755,6 +1860,15 @@ def test_read_config_with_custom_params(model: capellambse.MelodyModel):
     def test_read_config_links(caplog: pytest.LogCaptureFixture):
         caplog.set_level("DEBUG")
         config = converter_config.ConverterConfig()
+        expected = (
+            "capella2polarion.converters.converter_config",
+            20,
+            "Global link parent is not available on Capella type diagram",
+            "capella2polarion.converters.converter_config",
+            40,
+            "Link exchanged_items is not available on Capella type "
+            "FunctionalExchange",
+        )
         with open(TEST_MODEL_ELEMENTS_CONFIG, "r", encoding="utf8") as f:
             config.read_config_file(f)
 
@@ -1764,13 +1878,4 @@ def test_read_config_links(caplog: pytest.LogCaptureFixture):
             for link in config.diagram_config.links
             if link.capella_attr == "parent"
         )
-        assert caplog.record_tuples[0][1] == 20
-        assert (
-            caplog.record_tuples[0][2]
-            == "Global link parent is not available on Capella type diagram"
-        )
-        assert caplog.record_tuples[1][1] == 40
-        assert (
-            caplog.record_tuples[1][2]
-            == "Link exchanged_items is not available on Capella type FunctionalExchange"
-        )
+        assert caplog.record_tuples[0] + caplog.record_tuples[1] == expected

From 654eca90ea226f970832b32a089b3fe3f2b8ce51 Mon Sep 17 00:00:00 2001
From: ewuerger <ernst.wuerger@gmail.com>
Date: Tue, 20 Aug 2024 16:10:25 +0200
Subject: [PATCH 6/6] fix: Apply changes from code review

---
 .../converters/converter_config.py            |  16 +--
 capella2polarion/converters/link_converter.py |  31 ++--
 tests/test_elements.py                        | 136 +++++++++---------
 3 files changed, 93 insertions(+), 90 deletions(-)

diff --git a/capella2polarion/converters/converter_config.py b/capella2polarion/converters/converter_config.py
index 9f45d67c..d81ab52c 100644
--- a/capella2polarion/converters/converter_config.py
+++ b/capella2polarion/converters/converter_config.py
@@ -286,27 +286,25 @@ def _filter_links(
 
     available_links = []
     for link in links:
-        cappela_attr = link.capella_attr.split(".")[0]
+        capella_attr = link.capella_attr.split(".")[0]
+        is_diagram_elements = capella_attr == DIAGRAM_ELEMENTS_SERIALIZER
         if (
-            cappela_attr.endswith(DESCRIPTION_REFERENCE_SERIALIZER)
-            or (
-                cappela_attr.endswith(DIAGRAM_ELEMENTS_SERIALIZER)
-                and c_class == diagram.Diagram
-            )
-            or hasattr(c_class, cappela_attr)
+            capella_attr == DESCRIPTION_REFERENCE_SERIALIZER
+            or (is_diagram_elements and c_class == diagram.Diagram)
+            or hasattr(c_class, capella_attr)
         ):
             available_links.append(link)
         else:
             if is_global:
                 logger.info(
                     "Global link %s is not available on Capella type %s",
-                    cappela_attr,
+                    capella_attr,
                     c_type,
                 )
             else:
                 logger.error(
                     "Link %s is not available on Capella type %s",
-                    cappela_attr,
+                    capella_attr,
                     c_type,
                 )
     return available_links
diff --git a/capella2polarion/converters/link_converter.py b/capella2polarion/converters/link_converter.py
index 4e4d46b1..34256173 100644
--- a/capella2polarion/converters/link_converter.py
+++ b/capella2polarion/converters/link_converter.py
@@ -61,9 +61,8 @@ def create_links_for_work_item(
         new_links: list[polarion_api.WorkItemLink] = []
         link_errors: list[str] = []
         for link_config in converter_data.type_config.links:
-            assert (role_id := link_config.polarion_role) is not None
-            attr_id = link_config.capella_attr or ""
-            serializer = self.serializers.get(attr_id)
+            serializer = self.serializers.get(link_config.capella_attr)
+            role_id = link_config.polarion_role
             if self.role_prefix:
                 role_id = f"{self.role_prefix}_{role_id}"
             try:
@@ -72,7 +71,7 @@ def create_links_for_work_item(
                         serializer(obj, work_item.id, role_id, {})
                     )
                 else:
-                    refs = _resolve_attribute(obj, attr_id)
+                    refs = _resolve_attribute(obj, link_config.capella_attr)
                     new: cabc.Iterable[str]
                     if isinstance(refs, common.ElementList):
                         new = refs.by_uuid  # type: ignore[assignment]
@@ -87,9 +86,14 @@ def create_links_for_work_item(
                         self._create(work_item.id, role_id, new, {})
                     )
             except Exception as error:
-                request_text = f"Requested attribute: {attr_id}"
                 error_text = f"{type(error).__name__} {str(error)}"
-                link_errors.extend([request_text, error_text, "--------"])
+                link_errors.extend(
+                    [
+                        f"Requested attribute: {link_config.capella_attr}",
+                        error_text,
+                        "--------",
+                    ]
+                )
 
         if link_errors:
             for link_error in link_errors:
@@ -227,10 +231,7 @@ def create_grouped_link_fields(
                     config = link_config
                     break
 
-            role_id = role
-            if self.role_prefix:
-                role_id = role.removeprefix(f"{self.role_prefix}_")
-
+            role_id = self._remove_prefix(role)
             self._create_link_fields(
                 work_item, role_id, grouped_links, config=config
             )
@@ -315,12 +316,14 @@ def create_grouped_back_link_fields(
             List of links referencing work_item as secondary
         """
         for role, grouped_links in _group_by("role", links).items():
-            role_id = role
-            if self.role_prefix:
-                role_id = role.removeprefix(f"{self.role_prefix}_")
-
+            role_id = self._remove_prefix(role)
             self._create_link_fields(work_item, role_id, grouped_links, True)
 
+    def _remove_prefix(self, role: str) -> str:
+        if self.role_prefix:
+            return role.removeprefix(f"{self.role_prefix}_")
+        return role
+
 
 def _group_by(
     attr: str,
diff --git a/tests/test_elements.py b/tests/test_elements.py
index a594b8b3..f3e48604 100644
--- a/tests/test_elements.py
+++ b/tests/test_elements.py
@@ -162,6 +162,55 @@
 DIAGRAM_CONFIG = converter_config.CapellaTypeConfig("diagram", "diagram")
 
 
+class GroupedLinksBaseObject(t.TypedDict):
+    link_serializer: link_converter.LinkSerializer
+    work_items: dict[str, data_models.CapellaWorkItem]
+    back_links: dict[str, list[polarion_api.WorkItemLink]]
+    reverse_polarion_id_map: dict[str, str]
+    config: converter_config.CapellaTypeConfig
+
+
+# pylint: disable=redefined-outer-name
+@pytest.fixture()
+def grouped_links_base_object(
+    base_object: BaseObjectContainer,
+    dummy_work_items: dict[str, data_models.CapellaWorkItem],
+) -> GroupedLinksBaseObject:
+    reverse_polarion_id_map = {v: k for k, v in POLARION_ID_MAP.items()}
+    back_links: dict[str, list[polarion_api.WorkItemLink]] = {}
+    config = converter_config.CapellaTypeConfig(
+        "fakeModelObject",
+        links=[
+            converter_config.LinkConfig(
+                capella_attr="attribute", polarion_role="attribute"
+            )
+        ],
+    )
+    mock_model = mock.MagicMock()
+    fake_2 = FakeModelObject("uuid2", "Fake 2")
+    fake_1 = FakeModelObject("uuid1", "Fake 1")
+    fake_0 = FakeModelObject("uuid0", "Fake 0", attribute=[fake_1, fake_2])
+    fake_1.attribute = [fake_0, fake_2]
+    mock_model.by_uuid.side_effect = lambda uuid: {
+        "uuid0": fake_0,
+        "uuid1": fake_1,
+        "uuid2": fake_2,
+    }[uuid]
+    link_serializer = link_converter.LinkSerializer(
+        base_object.pw.polarion_data_repo,
+        base_object.mc.converter_session,
+        base_object.pw.polarion_params.project_id,
+        mock_model,
+    )
+    return {
+        "link_serializer": link_serializer,
+        "work_items": dummy_work_items,
+        "back_links": back_links,
+        "reverse_polarion_id_map": reverse_polarion_id_map,
+        "config": config,
+    }
+
+
 class TestDiagramElements:
     @staticmethod
     @pytest.fixture
@@ -1251,35 +1300,16 @@ def test_grouped_links_attributes_with_includes(
 
     @staticmethod
     def test_maintain_reverse_grouped_links_attributes(
-        base_object: BaseObjectContainer,
-        dummy_work_items: dict[str, data_models.CapellaWorkItem],
+        grouped_links_base_object: GroupedLinksBaseObject,
     ):
-        reverse_polarion_id_map = {v: k for k, v in POLARION_ID_MAP.items()}
-        back_links: dict[str, list[polarion_api.WorkItemLink]] = {}
-        config = converter_config.CapellaTypeConfig(
-            "fakeModelObject",
-            links=[
-                converter_config.LinkConfig(
-                    capella_attr="attribute", polarion_role="attribute"
-                )
-            ],
-        )
-        mock_model = mock.MagicMock()
-        fake_2 = FakeModelObject("uuid2", "Fake 2")
-        fake_1 = FakeModelObject("uuid1", "Fake 1")
-        fake_0 = FakeModelObject("uuid0", "Fake 0", attribute=[fake_1, fake_2])
-        fake_1.attribute = [fake_0, fake_2]
-        mock_model.by_uuid.side_effect = lambda uuid: {
-            "uuid0": fake_0,
-            "uuid1": fake_1,
-            "uuid2": fake_2,
-        }[uuid]
-        link_serializer = link_converter.LinkSerializer(
-            base_object.pw.polarion_data_repo,
-            base_object.mc.converter_session,
-            base_object.pw.polarion_params.project_id,
-            mock_model,
-        )
+        link_serializer = grouped_links_base_object["link_serializer"]
+        dummy_work_items = grouped_links_base_object["work_items"]
+        reverse_polarion_id_map = grouped_links_base_object[
+            "reverse_polarion_id_map"
+        ]
+        back_links = grouped_links_base_object["back_links"]
+        config = grouped_links_base_object["config"]
+
         for work_item in dummy_work_items.values():
             converter_data = data_session.ConverterData(
                 "test", config, [], work_item
@@ -1287,15 +1317,10 @@ def test_maintain_reverse_grouped_links_attributes(
             link_serializer.create_grouped_link_fields(
                 converter_data, back_links
             )
-
         for work_item_id, links in back_links.items():
             work_item = dummy_work_items[reverse_polarion_id_map[work_item_id]]
             link_serializer.create_grouped_back_link_fields(work_item, links)
-        del dummy_work_items["uuid0"].additional_attributes["uuid_capella"]
-        del dummy_work_items["uuid1"].additional_attributes["uuid_capella"]
-        del dummy_work_items["uuid2"].additional_attributes["uuid_capella"]
-        del dummy_work_items["uuid0"].additional_attributes["attribute"]
-        del dummy_work_items["uuid1"].additional_attributes["attribute"]
+
         assert (
             dummy_work_items["uuid0"].additional_attributes.pop(
                 "attribute_reverse"
@@ -1314,44 +1339,22 @@ def test_maintain_reverse_grouped_links_attributes(
             )["value"]
             == HTML_LINK_2["attribute_reverse"]
         )
-        assert dummy_work_items["uuid0"].additional_attributes == {}
-        assert dummy_work_items["uuid1"].additional_attributes == {}
-        assert dummy_work_items["uuid2"].additional_attributes == {}
 
     @staticmethod
     def test_maintain_reverse_grouped_links_attributes_with_role_prefix(
-        base_object: BaseObjectContainer,
-        dummy_work_items: dict[str, data_models.CapellaWorkItem],
+        grouped_links_base_object: GroupedLinksBaseObject,
     ):
-        reverse_polarion_id_map = {v: k for k, v in POLARION_ID_MAP.items()}
-        back_links: dict[str, list[polarion_api.WorkItemLink]] = {}
-        config = converter_config.CapellaTypeConfig(
-            "fakeModelObject",
-            links=[
-                converter_config.LinkConfig(
-                    capella_attr="attribute", polarion_role="attribute"
-                )
-            ],
-        )
-        mock_model = mock.MagicMock()
-        fake_2 = FakeModelObject("uuid2", "Fake 2")
-        fake_1 = FakeModelObject("uuid1", "Fake 1")
-        fake_0 = FakeModelObject("uuid0", "Fake 0", attribute=[fake_1, fake_2])
-        fake_1.attribute = [fake_0, fake_2]
-        mock_model.by_uuid.side_effect = lambda uuid: {
-            "uuid0": fake_0,
-            "uuid1": fake_1,
-            "uuid2": fake_2,
-        }[uuid]
+        link_serializer = grouped_links_base_object["link_serializer"]
+        dummy_work_items = grouped_links_base_object["work_items"]
+        reverse_polarion_id_map = grouped_links_base_object[
+            "reverse_polarion_id_map"
+        ]
+        back_links = grouped_links_base_object["back_links"]
+        config = grouped_links_base_object["config"]
         for link in dummy_work_items["uuid0"].linked_work_items:
             link.role = f"_C2P_{link.role}"
-        link_serializer = link_converter.LinkSerializer(
-            base_object.pw.polarion_data_repo,
-            base_object.mc.converter_session,
-            base_object.pw.polarion_params.project_id,
-            mock_model,
-            role_prefix="_C2P",
-        )
+        link_serializer.role_prefix = "_C2P"
+
         for work_item in dummy_work_items.values():
             converter_data = data_session.ConverterData(
                 "test", config, [], work_item
@@ -1359,7 +1362,6 @@ def test_maintain_reverse_grouped_links_attributes_with_role_prefix(
             link_serializer.create_grouped_link_fields(
                 converter_data, back_links
             )
-
         for work_item_id, links in back_links.items():
             work_item = dummy_work_items[reverse_polarion_id_map[work_item_id]]
             link_serializer.create_grouped_back_link_fields(work_item, links)