diff --git a/app/routes/issuer.py b/app/routes/issuer.py index 98223ed52..034387602 100644 --- a/app/routes/issuer.py +++ b/app/routes/issuer.py @@ -122,10 +122,7 @@ async def send_credential( f"Failed to send credential: {e.detail}", e.status_code ) from e - if result: - bound_logger.info("Successfully sent credential.") - else: - bound_logger.warning("No result from sending credential.") + bound_logger.info("Successfully sent credential.") return result @@ -212,10 +209,7 @@ async def create_offer( credential=credential, ) - if result: - bound_logger.info("Successfully created credential offer.") - else: - bound_logger.warning("No result from creating credential offer.") + bound_logger.info("Successfully created credential offer.") return result @@ -281,10 +275,7 @@ async def request_credential( controller=aries_controller, credential_exchange_id=credential_exchange_id ) - if result: - bound_logger.info("Successfully sent credential request.") - else: - bound_logger.warning("No result from sending credential request.") + bound_logger.info("Successfully sent credential request.") return result @@ -332,10 +323,7 @@ async def store_credential( controller=aries_controller, credential_exchange_id=credential_exchange_id ) - if result: - bound_logger.info("Successfully stored credential.") - else: - bound_logger.warning("No result from storing credential.") + bound_logger.info("Successfully stored credential.") return result @@ -459,10 +447,7 @@ async def get_credential( controller=aries_controller, credential_exchange_id=credential_exchange_id ) - if result: - bound_logger.info("Successfully fetched credential.") - else: - bound_logger.info("No credential returned.") + bound_logger.info("Successfully fetched credential.") return result @@ -582,7 +567,7 @@ async def get_credential_revocation_record( Raises: --- CloudApiException: 400 - If credential_exchange_id is not provided, BOTH credential_revocation_id and revocation_registry_id must be. + If credential_exchange_id is not provided, both credential_revocation_id and revocation_registry_id must be. """ bound_logger = logger.bind( body={ @@ -597,8 +582,8 @@ async def get_credential_revocation_record( credential_revocation_id is None or revocation_registry_id is None ): raise CloudApiException( - "If credential_exchange_id is not provided BOTH the credential_revocation_id and \ - revocation_registry_id MUST be provided.", + "If credential_exchange_id is not provided then both " + "credential_revocation_id and revocation_registry_id must be provided.", 400, ) @@ -611,10 +596,7 @@ async def get_credential_revocation_record( revocation_registry_id=revocation_registry_id, ) - if revocation_record: - bound_logger.info("Successfully fetched credential revocation record.") - else: - bound_logger.info("No credential revocation record returned.") + bound_logger.info("Successfully fetched credential revocation record.") return revocation_record diff --git a/app/tests/routes/issuer/test_clear_pending_revocations.py b/app/tests/routes/issuer/test_clear_pending_revocations.py new file mode 100644 index 000000000..da6551bb8 --- /dev/null +++ b/app/tests/routes/issuer/test_clear_pending_revocations.py @@ -0,0 +1,75 @@ +from unittest.mock import AsyncMock, patch + +import pytest +from aries_cloudcontroller.exceptions import ( + ApiException, + BadRequestException, + NotFoundException, +) +from fastapi import HTTPException + +from app.models.issuer import ClearPendingRevocationsRequest +from app.routes.issuer import clear_pending_revocations + + +@pytest.mark.anyio +async def test_clear_pending_revocations_success(): + mock_aries_controller = AsyncMock() + mock_clear_pending_revocations = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.services.revocation_registry.clear_pending_revocations", + mock_clear_pending_revocations, + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + clear_request = ClearPendingRevocationsRequest( + revocation_registry_credential_map={} + ) + + await clear_pending_revocations( + clear_pending_request=clear_request, auth="mocked_auth" + ) + + mock_clear_pending_revocations.assert_awaited_once_with( + controller=mock_aries_controller, revocation_registry_credential_map={} + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (BadRequestException, 400, "Bad request"), + (NotFoundException, 404, "Not found"), + (ApiException, 500, "Internal Server Error"), + ], +) +async def test_clear_pending_revocations_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + mock_aries_controller.revocation.clear_pending_revocations = AsyncMock( + side_effect=exception_class(status=expected_status_code, reason=expected_detail) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + HTTPException, match=expected_detail + ) as exc: + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + clear_request = ClearPendingRevocationsRequest( + revocation_registry_credential_map={} + ) + + await clear_pending_revocations( + clear_pending_request=clear_request, auth="mocked_auth" + ) + + assert exc.value.status_code == expected_status_code diff --git a/app/tests/routes/issuer/test_create_offer.py b/app/tests/routes/issuer/test_create_offer.py new file mode 100644 index 000000000..3d2243246 --- /dev/null +++ b/app/tests/routes/issuer/test_create_offer.py @@ -0,0 +1,152 @@ +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from aries_cloudcontroller import Credential, LDProofVCDetail, LDProofVCOptions +from aries_cloudcontroller.exceptions import ( + ApiException, + BadRequestException, + NotFoundException, +) +from fastapi import HTTPException + +from app.exceptions.cloudapi_exception import CloudApiException +from app.models.issuer import CreateOffer, CredentialType, IndyCredential +from app.routes.issuer import create_offer + +indy_cred = IndyCredential( + credential_definition_id="WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", attributes={} +) +ld_cred = LDProofVCDetail( + credential=Credential( + context=[ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + ], + type=["VerifiableCredential", "UniversityDegreeCredential"], + credentialSubject={ + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts", + }, + "college": "Faber College", + }, + issuanceDate="2021-04-12", + issuer="", + ), + options=LDProofVCOptions(proofType="Ed25519Signature2018"), +) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "credential", + [ + CreateOffer( + protocol_version="v2", + type=CredentialType.INDY, + indy_credential_detail=indy_cred, + ), + CreateOffer( + protocol_version="v2", + type=CredentialType.LD_PROOF, + ld_credential_detail=ld_cred, + ), + ], +) +async def test_create_offer_success(credential): + mock_aries_controller = AsyncMock() + issuer = Mock() + issuer.create_offer = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.routes.issuer.issuer_from_protocol_version", + return_value=issuer, + ), patch("app.routes.issuer.assert_public_did", return_value="public_did"), patch( + "app.routes.issuer.schema_id_from_credential_definition_id", + return_value="schema_id", + ), patch( + "app.routes.issuer.assert_valid_issuer" + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await create_offer(credential=credential, auth="mocked_auth") + + issuer.create_offer.assert_awaited_once_with( + controller=mock_aries_controller, credential=credential + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (BadRequestException, 400, "Bad request"), + (NotFoundException, 404, "Not found"), + (ApiException, 500, "Internal Server Error"), + ], +) +async def test_create_offer_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.create_offer = AsyncMock( + side_effect=exception_class(status=expected_status_code, reason=expected_detail) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + HTTPException, match=expected_detail + ) as exc, patch( + "app.routes.issuer.assert_public_did", return_value="public_did" + ), patch( + "app.routes.issuer.schema_id_from_credential_definition_id", + return_value="schema_id", + ), patch( + "app.routes.issuer.assert_valid_issuer" + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await create_offer( + credential=CreateOffer( + protocol_version="v2", + type=CredentialType.LD_PROOF, + ld_credential_detail=ld_cred, + ), + auth="mocked_auth", + ) + + assert exc.value.status_code == expected_status_code + + +@pytest.mark.anyio +async def test_create_offer_fail_bad_public_did(): + credential = CreateOffer( + protocol_version="v2", + type=CredentialType.INDY, + indy_credential_detail=indy_cred, + ) + + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.issue_credential_automated = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.routes.issuer.assert_public_did", + AsyncMock(side_effect=CloudApiException(status_code=404, detail="Not found")), + ), pytest.raises( + HTTPException, + match="Wallet making this request has no public DID. Only issuers with a public DID can make this request.", + ) as exc: + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await create_offer(credential=credential, auth="mocked_auth") + + mock_aries_controller.issue_credential_v2_0.issue_credential_automated.assert_awaited_once() + + assert exc.value.status_code == 403 diff --git a/app/tests/routes/issuer/test_get_credential.py b/app/tests/routes/issuer/test_get_credential.py new file mode 100644 index 000000000..f5acb7588 --- /dev/null +++ b/app/tests/routes/issuer/test_get_credential.py @@ -0,0 +1,62 @@ +from unittest.mock import AsyncMock, Mock, patch + +import pytest + +from app.exceptions.cloudapi_exception import CloudApiException +from app.routes.issuer import get_credential + + +@pytest.mark.anyio +async def test_get_credential_success(): + mock_aries_controller = AsyncMock() + issuer = Mock() + issuer.get_record = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.routes.issuer.issuer_from_id", return_value=issuer + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await get_credential(credential_exchange_id="test_id", auth="mocked_auth") + + issuer.get_record.assert_awaited_once_with( + controller=mock_aries_controller, credential_exchange_id="test_id" + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (CloudApiException, 400, "Bad request"), + (CloudApiException, 404, "Not found"), + (CloudApiException, 500, "Internal Server Error"), + ], +) +async def test_get_credential_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + issuer = Mock() + issuer.get_record = AsyncMock( + side_effect=exception_class( + status_code=expected_status_code, detail=expected_detail + ) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + CloudApiException, match=expected_detail + ) as exc, patch( + "app.routes.issuer.issuer_from_id", return_value=issuer + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await get_credential(credential_exchange_id="test_id", auth="mocked_auth") + + assert exc.value.status_code == expected_status_code diff --git a/app/tests/routes/issuer/test_get_credential_revocation_record.py b/app/tests/routes/issuer/test_get_credential_revocation_record.py new file mode 100644 index 000000000..b0eec7d05 --- /dev/null +++ b/app/tests/routes/issuer/test_get_credential_revocation_record.py @@ -0,0 +1,102 @@ +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from aries_cloudcontroller.exceptions import ( + ApiException, + BadRequestException, + NotFoundException, +) +from fastapi import HTTPException + +from app.routes.issuer import get_credential_revocation_record + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "credential_exchange_id, credential_revocation_id, revocation_registry_id", + [("a", None, None), (None, "b", "c")], +) +async def test_get_credential_revocation_record_success( + credential_exchange_id, credential_revocation_id, revocation_registry_id +): + mock_aries_controller = AsyncMock() + mock_get_revocation_record = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.services.revocation_registry.get_credential_revocation_record", + mock_get_revocation_record, + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await get_credential_revocation_record( + credential_exchange_id, + credential_revocation_id, + revocation_registry_id, + auth="mocked_auth", + ) + + mock_get_revocation_record.assert_awaited_once_with( + controller=mock_aries_controller, + credential_exchange_id=credential_exchange_id, + credential_revocation_id=credential_revocation_id, + revocation_registry_id=revocation_registry_id, + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (BadRequestException, 400, "Bad request"), + (NotFoundException, 404, "Not found"), + (ApiException, 500, "Internal Server Error"), + ], +) +async def test_get_credential_revocation_record_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + mock_aries_controller.revocation.get_revocation_status = AsyncMock( + side_effect=exception_class(status=expected_status_code, reason=expected_detail) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + HTTPException, match=expected_detail + ) as exc: + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await get_credential_revocation_record( + credential_exchange_id=MagicMock(), auth="mocked_auth" + ) + + assert exc.value.status_code == expected_status_code + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "credential_exchange_id, credential_revocation_id, revocation_registry_id", + [(None, None, None), (None, None, "c"), (None, "b", None)], +) +async def test_get_credential_revocation_record_fail_bad_request( + credential_exchange_id, credential_revocation_id, revocation_registry_id +): + with pytest.raises( + HTTPException, + match="If credential_exchange_id is not provided then both " + "credential_revocation_id and revocation_registry_id must be provided.", + ) as exc: + + await get_credential_revocation_record( + credential_exchange_id=credential_exchange_id, + credential_revocation_id=credential_revocation_id, + revocation_registry_id=revocation_registry_id, + auth="mocked_auth", + ) + + assert exc.value.status_code == 400 diff --git a/app/tests/routes/issuer/test_get_credentials.py b/app/tests/routes/issuer/test_get_credentials.py new file mode 100644 index 000000000..babe9501e --- /dev/null +++ b/app/tests/routes/issuer/test_get_credentials.py @@ -0,0 +1,66 @@ +from unittest.mock import AsyncMock, patch + +import pytest +from fastapi import HTTPException + +from app.exceptions.cloudapi_exception import CloudApiException +from app.routes.issuer import get_credentials + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "mock_v1_records, mock_v2_records", + [([], []), (["v1_rec"], []), ([], ["v2_rec"]), (["v1_rec"], ["v2_rec"])], +) +async def test_get_credentials_success(mock_v1_records, mock_v2_records): + mock_aries_controller = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.routes.issuer.IssueCredentialFacades.V1.value.get_records", + return_value=mock_v1_records, + ), patch( + "app.routes.issuer.IssueCredentialFacades.V2.value.get_records", + return_value=mock_v2_records, + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + response = await get_credentials(state=None, auth="mocked_auth") + + assert response == mock_v1_records + mock_v2_records + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (CloudApiException, 400, "Bad request"), + (CloudApiException, 404, "Not found"), + (CloudApiException, 500, "Internal Server Error"), + ], +) +async def test_get_credentials_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + HTTPException, match=expected_detail + ) as exc, patch( + "app.routes.issuer.IssueCredentialFacades.V1.value.get_records", + AsyncMock( + side_effect=exception_class( + status_code=expected_status_code, detail=expected_detail + ) + ), + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await get_credentials(state=None, auth="mocked_auth") + + assert exc.value.status_code == expected_status_code diff --git a/app/tests/routes/issuer/test_publish_revocations.py b/app/tests/routes/issuer/test_publish_revocations.py new file mode 100644 index 000000000..89f34b9bf --- /dev/null +++ b/app/tests/routes/issuer/test_publish_revocations.py @@ -0,0 +1,106 @@ +import asyncio +from unittest.mock import AsyncMock, patch + +import pytest + +from app.exceptions.cloudapi_exception import CloudApiException +from app.models.issuer import PublishRevocationsRequest +from app.routes.issuer import publish_revocations + + +@pytest.mark.anyio +async def test_publish_revocations_success(): + mock_aries_controller = AsyncMock() + mock_publish_revocations = AsyncMock(return_value="transaction_id") + mock_get_transaction = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.services.revocation_registry.publish_pending_revocations", + mock_publish_revocations, + ), patch( + "app.routes.issuer.coroutine_with_retry_until_value", mock_get_transaction + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + publish_request = PublishRevocationsRequest( + revocation_registry_credential_map={} + ) + + await publish_revocations(publish_request=publish_request, auth="mocked_auth") + + mock_publish_revocations.assert_awaited_once_with( + controller=mock_aries_controller, revocation_registry_credential_map={} + ) + mock_get_transaction.assert_awaited_once() + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (CloudApiException, 400, "Bad request"), + (CloudApiException, 404, "Not found"), + (CloudApiException, 500, "Internal Server Error"), + ], +) +async def test_publish_revocations_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + mock_publish_revocations = AsyncMock( + side_effect=exception_class( + status_code=expected_status_code, detail=expected_detail + ) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + CloudApiException, match=expected_detail + ) as exc, patch( + "app.services.revocation_registry.publish_pending_revocations", + mock_publish_revocations, + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + publish_request = PublishRevocationsRequest( + revocation_registry_credential_map={} + ) + + await publish_revocations(publish_request=publish_request, auth="mocked_auth") + + assert exc.value.status_code == expected_status_code + + +@pytest.mark.anyio +async def test_publish_revocations_fail_timeout(): + mock_aries_controller = AsyncMock() + mock_publish_revocations = AsyncMock(return_value="transaction_id") + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + CloudApiException, + match="Timeout waiting for endorser to accept the revocations request.", + ) as exc, patch( + "app.services.revocation_registry.publish_pending_revocations", + mock_publish_revocations, + ), patch( + "app.routes.issuer.coroutine_with_retry_until_value", + AsyncMock(side_effect=asyncio.TimeoutError()), + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + publish_request = PublishRevocationsRequest( + revocation_registry_credential_map={} + ) + + await publish_revocations(publish_request=publish_request, auth="mocked_auth") + + assert exc.value.status_code == 504 diff --git a/app/tests/routes/issuer/test_remove_credential_exchange_record.py b/app/tests/routes/issuer/test_remove_credential_exchange_record.py new file mode 100644 index 000000000..ab22e41d2 --- /dev/null +++ b/app/tests/routes/issuer/test_remove_credential_exchange_record.py @@ -0,0 +1,63 @@ +from unittest.mock import AsyncMock, patch + +import pytest +from aries_cloudcontroller.exceptions import ( + ApiException, + BadRequestException, + NotFoundException, +) +from fastapi import HTTPException + +from app.routes.issuer import remove_credential_exchange_record + + +@pytest.mark.anyio +async def test_remove_credential_exchange_record_success(): + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.delete_record = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth: + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await remove_credential_exchange_record( + credential_exchange_id="v2-test_id", auth="mocked_auth" + ) + + mock_aries_controller.issue_credential_v2_0.delete_record.assert_awaited_once_with( + cred_ex_id="test_id" + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (BadRequestException, 400, "Bad request"), + (NotFoundException, 404, "Not found"), + (ApiException, 500, "Internal Server Error"), + ], +) +async def test_remove_credential_exchange_record_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.delete_record = AsyncMock( + side_effect=exception_class(status=expected_status_code, reason=expected_detail) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + HTTPException, match=expected_detail + ) as exc: + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await remove_credential_exchange_record( + credential_exchange_id="v2-test_id", auth="mocked_auth" + ) + + assert exc.value.status_code == expected_status_code diff --git a/app/tests/routes/issuer/test_request_credential.py b/app/tests/routes/issuer/test_request_credential.py new file mode 100644 index 000000000..7d7262d27 --- /dev/null +++ b/app/tests/routes/issuer/test_request_credential.py @@ -0,0 +1,144 @@ +from unittest.mock import AsyncMock, Mock, patch + +import pytest +from aries_cloudcontroller import V20CredRequestRequest +from aries_cloudcontroller.exceptions import ( + ApiException, + BadRequestException, + NotFoundException, +) +from fastapi import HTTPException + +from app.routes.issuer import request_credential +from app.services.issuer.acapy_issuer_v2 import IssuerV2 + + +@pytest.mark.anyio +@pytest.mark.parametrize("record_type", ["indy", "ld_proof", "bad"]) +async def test_request_credential_success(record_type): + mock_aries_controller = AsyncMock() + issuer = Mock() + issuer.request_credential = IssuerV2.request_credential + + record = Mock() + record.type = record_type + issuer.get_record = AsyncMock(return_value=record) + + mock_aries_controller.issue_credential_v2_0.send_request = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.routes.issuer.issuer_from_id", return_value=issuer + ), patch( + "app.routes.issuer.did_from_credential_definition_id", return_value="issuer_did" + ), patch( + "app.routes.issuer.qualified_did_sov", return_value="qualified_did_sov" + ), patch( + "app.routes.issuer.assert_valid_issuer" + ), patch( + "app.services.issuer.acapy_issuer_v2.credential_record_to_model_v2" + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + if record_type == "bad": + with pytest.raises( + HTTPException, match="Could not resolve record type" + ) as exc: + await request_credential( + credential_exchange_id="v2-test_id", auth="mocked_auth" + ) + + assert exc.value.status_code == 500 + + else: + await request_credential( + credential_exchange_id="v2-test_id", auth="mocked_auth" + ) + + mock_aries_controller.issue_credential_v2_0.send_request.assert_awaited_once_with( + cred_ex_id="test_id", body=V20CredRequestRequest() + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (BadRequestException, 400, "Bad request"), + (NotFoundException, 404, "Not found"), + (ApiException, 500, "Internal Server Error"), + ], +) +async def test_request_credential_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + issuer = Mock() + issuer.request_credential = IssuerV2.request_credential + + record = Mock() + record.type = "indy" + issuer.get_record = AsyncMock(return_value=record) + + mock_aries_controller.issue_credential_v2_0.send_request = AsyncMock( + side_effect=exception_class(status=expected_status_code, reason=expected_detail) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + HTTPException, match=expected_detail + ) as exc, patch( + "app.routes.issuer.issuer_from_id", return_value=issuer + ), patch( + "app.services.issuer.acapy_issuer_v2.credential_record_to_model_v2" + ), patch( + "app.routes.issuer.did_from_credential_definition_id", return_value="issuer_did" + ), patch( + "app.routes.issuer.qualified_did_sov", return_value="qualified_did_sov" + ), patch( + "app.routes.issuer.assert_valid_issuer" + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await request_credential( + credential_exchange_id="v2-test_id", auth="mocked_auth" + ) + + assert exc.value.status_code == expected_status_code + + +@pytest.mark.anyio +async def test_request_credential_fail_bad_record(): + mock_aries_controller = AsyncMock() + issuer = Mock() + issuer.request_credential = IssuerV2.request_credential + + record = Mock() + record.type = "indy" + record.credential_definition_id = None + issuer.get_record = AsyncMock(return_value=record) + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.routes.issuer.issuer_from_id", return_value=issuer + ), patch( + "app.services.issuer.acapy_issuer_v2.credential_record_to_model_v2" + ), pytest.raises( + HTTPException, + match=( + "Record has no credential definition or schema associated. " + "This probably means you haven't received an offer yet." + ), + ) as exc: + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await request_credential( + credential_exchange_id="v2-test_id", auth="mocked_auth" + ) + + assert exc.value.status_code == 412 diff --git a/app/tests/routes/issuer/test_revoke_credential.py b/app/tests/routes/issuer/test_revoke_credential.py new file mode 100644 index 000000000..9ba92f8d2 --- /dev/null +++ b/app/tests/routes/issuer/test_revoke_credential.py @@ -0,0 +1,75 @@ +from unittest.mock import AsyncMock, patch + +import pytest + +from app.exceptions.cloudapi_exception import CloudApiException +from app.models.issuer import RevokeCredential +from app.routes.issuer import revoke_credential + +credential_exchange_id = "v2-db9d7025-b276-4c32-ae38-fbad41864112" + + +@pytest.mark.anyio +@pytest.mark.parametrize("auto_publish_to_ledger", [True, False]) +async def test_revoke_credential_success(auto_publish_to_ledger): + mock_aries_controller = AsyncMock() + mock_revoke_credential = AsyncMock() + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.services.revocation_registry.revoke_credential", mock_revoke_credential + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + request_body = RevokeCredential( + credential_exchange_id=credential_exchange_id, + auto_publish_on_ledger=auto_publish_to_ledger, + ) + + await revoke_credential(body=request_body, auth="mocked_auth") + + mock_revoke_credential.assert_awaited_once_with( + controller=mock_aries_controller, + credential_exchange_id=credential_exchange_id, + auto_publish_to_ledger=auto_publish_to_ledger, + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (CloudApiException, 400, "Bad request"), + (CloudApiException, 404, "Not found"), + (CloudApiException, 500, "Internal Server Error"), + ], +) +async def test_revoke_credential_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + mock_revoke_credential = AsyncMock( + side_effect=exception_class( + status_code=expected_status_code, detail=expected_detail + ) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + CloudApiException, match=expected_detail + ) as exc, patch( + "app.services.revocation_registry.revoke_credential", mock_revoke_credential + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + request_body = RevokeCredential( + credential_exchange_id=credential_exchange_id, + auto_publish_on_ledger=False, + ) + + await revoke_credential(body=request_body, auth="mocked_auth") + + assert exc.value.status_code == expected_status_code diff --git a/app/tests/routes/issuer/test_send_credential.py b/app/tests/routes/issuer/test_send_credential.py new file mode 100644 index 000000000..9ad66f212 --- /dev/null +++ b/app/tests/routes/issuer/test_send_credential.py @@ -0,0 +1,131 @@ +from unittest.mock import AsyncMock, patch + +import pytest +from aries_cloudcontroller.exceptions import ( + ApiException, + BadRequestException, + NotFoundException, +) +from fastapi import HTTPException + +from app.exceptions.cloudapi_exception import CloudApiException +from app.models.issuer import CredentialType, SendCredential +from app.routes.issuer import send_credential +from app.tests.routes.issuer.test_create_offer import indy_cred, ld_cred + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "credential", + [ + SendCredential( + protocol_version="v2", + type=CredentialType.INDY, + indy_credential_detail=indy_cred, + connection_id="abc", + ), + SendCredential( + protocol_version="v2", + type=CredentialType.LD_PROOF, + ld_credential_detail=ld_cred, + connection_id="abc", + ), + ], +) +async def test_send_credential_success(credential): + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.issue_credential_automated = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.routes.issuer.assert_public_did", return_value="public_did" + ), patch( + "app.services.issuer.acapy_issuer_v2.credential_record_to_model_v2" + ), patch( + "app.routes.issuer.schema_id_from_credential_definition_id", + return_value="schema_id", + ), patch( + "app.routes.issuer.assert_valid_issuer" + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await send_credential(credential=credential, auth="mocked_auth") + + mock_aries_controller.issue_credential_v2_0.issue_credential_automated.assert_awaited_once() + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (BadRequestException, 400, "Bad request"), + (NotFoundException, 404, "Not found"), + (ApiException, 500, "Internal Server Error"), + ], +) +async def test_send_credential_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.issue_credential_automated = AsyncMock( + side_effect=exception_class(status=expected_status_code, reason=expected_detail) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + HTTPException, match=expected_detail + ) as exc, patch( + "app.routes.issuer.assert_public_did", return_value="public_did" + ), patch( + "app.routes.issuer.schema_id_from_credential_definition_id", + return_value="schema_id", + ), patch( + "app.routes.issuer.assert_valid_issuer" + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await send_credential( + credential=SendCredential( + protocol_version="v2", + type=CredentialType.INDY, + indy_credential_detail=indy_cred, + connection_id="abc", + ), + auth="mocked_auth", + ) + + assert exc.value.status_code == expected_status_code + + +@pytest.mark.anyio +async def test_send_credential_fail_bad_public_did(): + credential = SendCredential( + protocol_version="v2", + type=CredentialType.INDY, + indy_credential_detail=indy_cred, + connection_id="abc", + ) + + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.issue_credential_automated = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.routes.issuer.assert_public_did", + AsyncMock(side_effect=CloudApiException(status_code=404, detail="Not found")), + ), pytest.raises( + HTTPException, + match="Wallet making this request has no public DID. Only issuers with a public DID can make this request.", + ) as exc: + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await send_credential(credential=credential, auth="mocked_auth") + + mock_aries_controller.issue_credential_v2_0.issue_credential_automated.assert_awaited_once() + + assert exc.value.status_code == 403 diff --git a/app/tests/routes/issuer/test_store_credential.py b/app/tests/routes/issuer/test_store_credential.py new file mode 100644 index 000000000..a10a3e744 --- /dev/null +++ b/app/tests/routes/issuer/test_store_credential.py @@ -0,0 +1,62 @@ +from unittest.mock import AsyncMock, patch + +import pytest +from aries_cloudcontroller import V20CredStoreRequest +from aries_cloudcontroller.exceptions import ( + ApiException, + BadRequestException, + NotFoundException, +) +from fastapi import HTTPException + +from app.routes.issuer import store_credential + + +@pytest.mark.anyio +async def test_store_credential_success(): + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.store_credential = AsyncMock() + + with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + "app.services.issuer.acapy_issuer_v2.credential_record_to_model_v2" + ): + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await store_credential(credential_exchange_id="v2-test_id", auth="mocked_auth") + + mock_aries_controller.issue_credential_v2_0.store_credential.assert_awaited_once_with( + cred_ex_id="test_id", body=V20CredStoreRequest() + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "exception_class, expected_status_code, expected_detail", + [ + (BadRequestException, 400, "Bad request"), + (NotFoundException, 404, "Not found"), + (ApiException, 500, "Internal Server Error"), + ], +) +async def test_store_credential_fail_acapy_error( + exception_class, expected_status_code, expected_detail +): + mock_aries_controller = AsyncMock() + mock_aries_controller.issue_credential_v2_0.store_credential = AsyncMock( + side_effect=exception_class(status=expected_status_code, reason=expected_detail) + ) + + with patch( + "app.routes.issuer.client_from_auth" + ) as mock_client_from_auth, pytest.raises( + HTTPException, match=expected_detail + ) as exc: + mock_client_from_auth.return_value.__aenter__.return_value = ( + mock_aries_controller + ) + + await store_credential(credential_exchange_id="v2-test_id", auth="mocked_auth") + + assert exc.value.status_code == expected_status_code