diff --git a/acapy_agent/askar/profile.py b/acapy_agent/askar/profile.py index e562ff88ff..4afcea0d60 100644 --- a/acapy_agent/askar/profile.py +++ b/acapy_agent/askar/profile.py @@ -178,11 +178,15 @@ def bind_providers(self): ), ) - def session(self, context: Optional[InjectionContext] = None) -> ProfileSession: + def session( + self, context: Optional[InjectionContext] = None + ) -> "AskarProfileSession": """Start a new interactive session with no transaction support requested.""" return AskarProfileSession(self, False, context=context) - def transaction(self, context: Optional[InjectionContext] = None) -> ProfileSession: + def transaction( + self, context: Optional[InjectionContext] = None + ) -> "AskarProfileSession": """Start a new interactive session with commit and rollback support. If the current backend does not support transactions, then commit diff --git a/acapy_agent/indy/credx/holder.py b/acapy_agent/indy/credx/holder.py index 089f1620d1..31d4bb67d3 100644 --- a/acapy_agent/indy/credx/holder.py +++ b/acapy_agent/indy/credx/holder.py @@ -67,6 +67,7 @@ def profile(self) -> AskarProfile: async def get_link_secret(self) -> LinkSecret: """Get or create the default link secret.""" + LOGGER.debug("Attempting to fetch or create the link secret.") while True: async with self._profile.session() as session: @@ -75,30 +76,51 @@ async def get_link_secret(self) -> LinkSecret: CATEGORY_LINK_SECRET, IndyCredxHolder.LINK_SECRET_ID ) except AskarError as err: + LOGGER.error("Error fetching link secret: %s", err) raise IndyHolderError("Error fetching link secret") from err + if record: try: + LOGGER.debug("Loading LinkSecret") secret = LinkSecret.load(record.raw_value) + LOGGER.debug("Loaded existing link secret.") except CredxError as err: - raise IndyHolderError("Error loading link secret") from err + LOGGER.info( + "Attempt fallback method after error loading link secret: %s", + err, + ) + try: + ms_string = record.value.decode("ascii") + link_secret_dict = {"value": {"ms": ms_string}} + secret = LinkSecret.load(link_secret_dict) + LOGGER.debug("Loaded LinkSecret from Anoncreds secret.") + except CredxError as decode_err: + LOGGER.error("Error loading link secret: %s", decode_err) + raise IndyHolderError("Error loading link secret") from err break else: try: secret = LinkSecret.create() + LOGGER.debug("Created new link secret.") except CredxError as err: + LOGGER.error("Error creating link secret: %s", err) raise IndyHolderError("Error creating link secret") from err + try: await session.handle.insert( CATEGORY_LINK_SECRET, IndyCredxHolder.LINK_SECRET_ID, secret.to_json_buffer(), ) + LOGGER.debug("Saved new link secret.") except AskarError as err: if err.code != AskarErrorCode.DUPLICATE: + LOGGER.error("Error saving link secret: %s", err) raise IndyHolderError("Error saving link secret") from err # else: lost race to create record, retry else: break + LOGGER.debug("Returning link secret.") return secret async def create_credential_request( diff --git a/acapy_agent/indy/credx/tests/test_get_link_secret.py b/acapy_agent/indy/credx/tests/test_get_link_secret.py new file mode 100644 index 0000000000..d34a32cbb6 --- /dev/null +++ b/acapy_agent/indy/credx/tests/test_get_link_secret.py @@ -0,0 +1,130 @@ +from unittest import IsolatedAsyncioTestCase + +import pytest +from aries_askar import AskarError, AskarErrorCode + +from acapy_agent.indy.credx.holder import CredxError, IndyHolderError +from acapy_agent.utils.testing import create_test_profile + +from ....tests import mock +from .. import holder + + +@pytest.mark.askar +@pytest.mark.indy_credx +class TestIndyCredxGetLinkSecret(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.holder_profile = await create_test_profile() + self.holder = holder.IndyCredxHolder(self.holder_profile) + + self.mock_session = mock.MagicMock() + self.mock_session.__aenter__.return_value = self.mock_session + self.mock_session.__aexit__.return_value = None + self.holder._profile.session = mock.MagicMock(return_value=self.mock_session) + + @mock.patch("acapy_agent.indy.credx.holder.LinkSecret.load") + @mock.patch("acapy_agent.indy.credx.holder.LinkSecret.create") + async def test_get_link_secret_existing(self, mock_create, mock_load): + # Mock session and record + mock_record = mock.MagicMock() + mock_record.raw_value = b'{"value": {"ms": "mocked_ms"}}' + + self.mock_session.handle.fetch = mock.CoroutineMock(return_value=mock_record) + + # Test fetching existing link secret + secret = await self.holder.get_link_secret() + assert secret is not None + mock_load.assert_called_once_with(mock_record.raw_value) + mock_create.assert_not_called() + + @mock.patch("acapy_agent.indy.credx.holder.LinkSecret.load") + async def test_get_link_secret_fetch_error(self, mock_load): + # Mock session to raise an error + self.mock_session.handle.fetch = mock.CoroutineMock( + side_effect=AskarError(AskarErrorCode.BACKEND, "Fetch error") + ) + + with pytest.raises(IndyHolderError, match="Error fetching link secret"): + await self.holder.get_link_secret() + + mock_load.assert_not_called() + + @mock.patch("acapy_agent.indy.credx.holder.LinkSecret.load") + async def test_get_link_secret_load_error(self, mock_load): + # Mock session and record + mock_record = mock.MagicMock() + mock_record.raw_value = b'{"value": {"ms": "mocked_ms"}}' + self.mock_session.handle.fetch = mock.CoroutineMock(return_value=mock_record) + + # Mock load to raise an error + mock_load.side_effect = CredxError(4, "Load error") + + with pytest.raises(IndyHolderError, match="Error loading link secret"): + await self.holder.get_link_secret() + + @mock.patch("acapy_agent.indy.credx.holder.LinkSecret.load") + async def test_get_link_secret_fallback_load(self, mock_load): + # Mock session and record + mock_record = mock.MagicMock() + mock_record.raw_value = b'{"value": {"ms": "mocked_ms"}}' + self.mock_session.handle.fetch = mock.CoroutineMock(return_value=mock_record) + + # Mock load to raise an error initially + mock_load.side_effect = [CredxError(4, "Load error"), mock.MagicMock()] + + # Test fallback method + secret = await self.holder.get_link_secret() + assert secret is not None + assert mock_load.call_count == 2 + + @mock.patch("acapy_agent.indy.credx.holder.LinkSecret.create") + async def test_get_link_secret_create_error(self, mock_create): + # Mock session to return no record + self.mock_session.handle.fetch = mock.CoroutineMock(return_value=None) + + # Mock create to raise an error + mock_create.side_effect = CredxError(4, "Create error") + + with pytest.raises(IndyHolderError, match="Error creating link secret"): + await self.holder.get_link_secret() + + @mock.patch("acapy_agent.indy.credx.holder.LinkSecret.create") + async def test_get_link_secret_create_and_save(self, mock_create): + # Mock session to return no record + self.mock_session.handle.fetch = mock.CoroutineMock(return_value=None) + + # Mock successful creation + mock_secret = mock.MagicMock() + mock_create.return_value = mock_secret + + # Mock successful insert + self.mock_session.handle.insert = mock.CoroutineMock() + + # Test creating and saving new link secret + secret = await self.holder.get_link_secret() + assert secret is not None + mock_create.assert_called_once() + self.mock_session.handle.insert.assert_called_once() + + @mock.patch("acapy_agent.indy.credx.holder.LinkSecret.create") + async def test_get_link_secret_duplicate_error(self, mock_create): + # Mock session to return no record + self.mock_session.handle.fetch = mock.CoroutineMock(return_value=None) + + # Mock successful creation + mock_secret = mock.MagicMock() + mock_create.return_value = mock_secret + + # Mock insert to raise a duplicate error + self.mock_session.handle.insert = mock.CoroutineMock( + side_effect=[ + AskarError(AskarErrorCode.DUPLICATE, "Duplicate error"), + mock.CoroutineMock(), + ] + ) + + # Test handling of duplicate error + secret = await self.holder.get_link_secret() + assert secret is not None + assert mock_create.call_count == 2 + assert self.mock_session.handle.insert.call_count == 2 diff --git a/acapy_agent/protocols/present_proof/v2_0/formats/indy/handler.py b/acapy_agent/protocols/present_proof/v2_0/formats/indy/handler.py index bac3933919..2400b60213 100644 --- a/acapy_agent/protocols/present_proof/v2_0/formats/indy/handler.py +++ b/acapy_agent/protocols/present_proof/v2_0/formats/indy/handler.py @@ -36,17 +36,11 @@ class IndyPresExchangeHandler(V20PresFormatHandler): """Indy presentation format handler.""" format = V20PresFormat.Format.INDY - anoncreds_handler = None def __init__(self, profile: Profile): """Shim initialization to check for new AnonCreds library.""" super().__init__(profile) - # Temporary shim while the new anoncreds library integration is in progress - wallet_type = profile.settings.get_value("wallet.type") - if wallet_type == "askar-anoncreds": - self.anoncreds_handler = AnonCredsPresExchangeHandler(profile) - @classmethod def validate_fields(cls, message_type: str, attachment_data: Mapping): """Validate attachment data for a specific message type. @@ -120,12 +114,6 @@ async def create_bound_request( A tuple (updated presentation exchange record, presentation request message) """ - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return await self.anoncreds_handler.create_bound_request( - pres_ex_record, - request_data, - ) indy_proof_request = pres_ex_record.pres_proposal.attachment( IndyPresExchangeHandler.format @@ -149,12 +137,6 @@ async def create_pres( ) -> Tuple[V20PresFormat, AttachDecorator]: """Create a presentation.""" - if self.anoncreds_handler: - return await self.anoncreds_handler.create_pres( - pres_ex_record, - request_data, - ) - requested_credentials = {} if not request_data: try: @@ -323,10 +305,6 @@ def _check_proof_vs_proposal(): f"restrictions {req_restrictions}" ) - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return await self.anoncreds_handler.receive_pres(message, pres_ex_record) - proof = message.attachment(IndyPresExchangeHandler.format) _check_proof_vs_proposal() @@ -341,10 +319,6 @@ async def verify_pres(self, pres_ex_record: V20PresExRecord) -> V20PresExRecord: presentation exchange record, updated """ - # Temporary shim while the new anoncreds library integration is in progress - if self.anoncreds_handler: - return await self.anoncreds_handler.verify_pres(pres_ex_record) - pres_request_msg = pres_ex_record.pres_request # The `or` anoncreds format is for the indy <--> anoncreds compatibility