diff --git a/app/main.py b/app/main.py index 4b7aa3929..2609a1ba8 100644 --- a/app/main.py +++ b/app/main.py @@ -18,6 +18,7 @@ jsonld, messaging, oob, + revocation, sse, trust_registry, verifier, @@ -58,6 +59,7 @@ connections, definitions, issuer, + revocation, jsonld, messaging, oob, diff --git a/app/routes/issuer.py b/app/routes/issuer.py index 2bbae4e36..d315eb353 100644 --- a/app/routes/issuer.py +++ b/app/routes/issuer.py @@ -1,25 +1,12 @@ -import asyncio from typing import List, Optional from uuid import UUID -from aries_cloudcontroller import IssuerCredRevRecord from fastapi import APIRouter, Depends, Query from app.dependencies.acapy_clients import client_from_auth from app.dependencies.auth import AcaPyAuth, acapy_auth_from_header from app.exceptions import CloudApiException -from app.models.issuer import ( - ClearPendingRevocationsRequest, - ClearPendingRevocationsResult, - CreateOffer, - CredentialType, - PendingRevocations, - PublishRevocationsRequest, - RevokeCredential, - RevokedResponse, - SendCredential, -) -from app.services import revocation_registry +from app.models.issuer import CreateOffer, CredentialType, SendCredential from app.services.acapy_ledger import schema_id_from_credential_definition_id from app.services.acapy_wallet import assert_public_did from app.services.issuer.acapy_issuer_v2 import IssuerV2 @@ -31,7 +18,6 @@ offset_query_parameter, order_by_query_parameter, ) -from app.util.retry_method import coroutine_with_retry_until_value from app.util.save_exchange_record import save_exchange_record_query from shared.log_config import get_logger from shared.models.credential_exchange import CredentialExchange, Role, State @@ -481,294 +467,3 @@ async def remove_credential_exchange_record( ) bound_logger.debug("Successfully deleted credential exchange record.") - - -@router.post("/revoke", summary="Revoke a Credential (if revocable)") -async def revoke_credential( - body: RevokeCredential, - auth: AcaPyAuth = Depends(acapy_auth_from_header), -) -> RevokedResponse: - """ - Revoke a credential - --- - Revoke a credential by providing the identifier of the exchange. - - If an issuer is going to revoke more than one credential, it is recommended to set the - 'auto_publish_on_ledger' field to False (default), and then batch publish the revocations using - the 'publish-revocations' endpoint. - - By batching the revocations, the issuer can save on transaction fees related to - publishing revocations to the ledger. - - Request Body: - --- - body: RevokeCredential - - credential_exchange_id (str): The ID associated with the credential exchange that should be revoked. - - auto_publish_on_ledger (bool): (True) publish revocation to ledger immediately, or - (default, False) mark it pending - - Returns: - --- - RevokedResponse: - revoked_cred_rev_ids: - The revocation registry indexes that were revoked. - Will be empty if the revocation was marked as pending. - """ - bound_logger = logger.bind(body=body) - bound_logger.debug("POST request received: Revoke credential") - - async with client_from_auth(auth) as aries_controller: - bound_logger.debug("Revoking credential") - result = await revocation_registry.revoke_credential( - controller=aries_controller, - credential_exchange_id=body.credential_exchange_id, - auto_publish_to_ledger=body.auto_publish_on_ledger, - ) - - bound_logger.debug("Successfully revoked credential.") - return result - - -@router.get( - "/revocation/record", - summary="Fetch a Revocation Record", - response_model=IssuerCredRevRecord, -) -async def get_credential_revocation_record( - credential_exchange_id: Optional[str] = None, - credential_revocation_id: Optional[str] = None, - revocation_registry_id: Optional[str] = None, - auth: AcaPyAuth = Depends(acapy_auth_from_header), -) -> IssuerCredRevRecord: - """ - Get a credential revocation record - --- - Fetch a credential revocation record by providing the credential exchange id. - Records can also be fetched by providing the credential revocation id and revocation registry id. - - The record is the payload of the webhook event on topic 'issuer_cred_rev', and contains the credential's revocation - status and other metadata. - - The revocation registry id (rev_reg_id) and credential revocation id (cred_rev_id) can be found - in this record if you have the credential exchange id. - - Parameters: - --- - credential_exchange_id: str - credential_revocation_id: str - revocation_registry_id: str - - Returns: - --- - IssuerCredRevRecord - The credential revocation record - - Raises: - --- - CloudApiException: 400 - If credential_exchange_id is not provided, both credential_revocation_id and revocation_registry_id must be. - """ - bound_logger = logger.bind( - body={ - "credential_exchange_id": credential_exchange_id, - "credential_revocation_id": credential_revocation_id, - "revocation_registry_id": revocation_registry_id, - } - ) - bound_logger.debug("GET request received: Get credential revocation record by id") - - if credential_exchange_id is None and ( - credential_revocation_id is None or revocation_registry_id is None - ): - raise CloudApiException( - "If credential_exchange_id is not provided then both " - "credential_revocation_id and revocation_registry_id must be provided.", - 400, - ) - - async with client_from_auth(auth) as aries_controller: - bound_logger.debug("Getting credential revocation record") - revocation_record = await revocation_registry.get_credential_revocation_record( - controller=aries_controller, - credential_exchange_id=credential_exchange_id, - credential_revocation_id=credential_revocation_id, - revocation_registry_id=revocation_registry_id, - ) - - bound_logger.debug("Successfully fetched credential revocation record.") - return revocation_record - - -@router.post("/publish-revocations", summary="Publish Pending Revocations") -async def publish_revocations( - publish_request: PublishRevocationsRequest, - auth: AcaPyAuth = Depends(acapy_auth_from_header), -) -> RevokedResponse: - """ - Write pending revocations to the ledger - --- - Revocations that are in a pending state can be published to the ledger. - - The endpoint accepts a `revocation_registry_credential_map`, which provides a dictionary of - revocation registry IDs to credential revocation IDs, to allow publishing individual credentials. - - If no revocation registry id is provided (i.e. an empty map `revocation_registry_credential_map: {}`), - then all pending revocations will be published. - - If no credential revocation id is provided under a given revocation registry id, then all pending revocations for - the given revocation registry id will be published. - - Where to find the revocation registry id and credential revocation id: - When issuing a credential, against a credential definition that supports revocation, - the issuer will receive a webhook event on the topic 'issuer_cred_rev'. This event will contain - the credential exchange id (cred_ex_id), the credential revocation id (cred_rev_id) and - the revocation registry id (rev_reg_id). - - Request Body: - --- - publish_request: PublishRevocationsRequest - An instance of `PublishRevocationsRequest` containing a `revocation_registry_credential_map`. This map - is a dictionary where each key is a revocation registry ID and its value is a list of credential - revocation IDs to be published. Providing an empty list for a registry ID instructs the system to - publish all pending revocations for that ID. An empty dictionary signifies that all pending - revocations across all registry IDs should be published. - - Returns: - --- - RevokedResponse: - revoked_cred_rev_ids: - The revocation registry indexes that were revoked. - Will be empty if there were no revocations to publish. - """ - bound_logger = logger.bind(body=publish_request) - bound_logger.debug("POST request received: Publish revocations") - - async with client_from_auth(auth) as aries_controller: - bound_logger.debug("Publishing revocations") - result = await revocation_registry.publish_pending_revocations( - controller=aries_controller, - revocation_registry_credential_map=publish_request.revocation_registry_credential_map, - ) - - if not result: - bound_logger.debug("No revocations to publish.") - return RevokedResponse() - - endorser_transaction_ids = [txn.transaction_id for txn in result.txn] - for endorser_transaction_id in endorser_transaction_ids: - bound_logger.debug( - "Wait for publish complete on transaction id: {}", - endorser_transaction_id, - ) - try: - # Wait for transaction to be acknowledged and written to the ledger - await coroutine_with_retry_until_value( - coroutine_func=aries_controller.endorse_transaction.get_transaction, - args=(endorser_transaction_id,), - field_name="state", - expected_value="transaction_acked", - logger=bound_logger, - max_attempts=30, - retry_delay=1, - ) - except asyncio.TimeoutError as e: - raise CloudApiException( - "Timeout waiting for endorser to accept the revocations request.", - 504, - ) from e - - bound_logger.debug("Successfully published revocations.") - return RevokedResponse.model_validate(result.model_dump()) - - -@router.post( - "/clear-pending-revocations", - summary="Clear Pending Revocations", - response_model=ClearPendingRevocationsResult, -) -async def clear_pending_revocations( - clear_pending_request: ClearPendingRevocationsRequest, - auth: AcaPyAuth = Depends(acapy_auth_from_header), -) -> ClearPendingRevocationsResult: - """ - Clear pending revocations - --- - Revocations that are in a pending state can be cleared, such that they are no longer set to be revoked. - - The endpoint accepts a `revocation_registry_credential_map`, which provides a dictionary of - revocation registry IDs to credential revocation IDs, to allow clearing individual credentials. - - If no revocation registry id is provided (i.e. an empty map `revocation_registry_credential_map: {}`), - then all pending revocations will be cleared. - - If no credential revocation id is provided under a given revocation registry id, then all pending revocations for - the given revocation registry id will be cleared. - - Where to find the revocation registry id and credential revocation id: - When issuing a credential, against a credential definition that supports revocation, - the issuer will receive a webhook event on the topic 'issuer_cred_rev'. This event will contain - the credential exchange id (cred_ex_id), the credential revocation id (cred_rev_id) and - the revocation registry id (rev_reg_id). - - Request Body: - --- - clear_pending_request: ClearPendingRevocationsRequest - An instance of `ClearPendingRevocationsRequest` containing a `revocation_registry_credential_map`. This map - is a dictionary where each key is a revocation registry ID and its value is a list of credential - revocation IDs to be cleared. Providing an empty list for a registry ID instructs the system to - clear all pending revocations for that ID. An empty dictionary signifies that all pending - revocations across all registry IDs should be cleared. - - Returns: - --- - ClearPendingRevocationsResult - The revocations that are still pending after the clear request is performed - """ - bound_logger = logger.bind(body=clear_pending_request) - bound_logger.debug("POST request received: Clear pending revocations") - - async with client_from_auth(auth) as aries_controller: - bound_logger.debug("Clearing pending revocations") - response = await revocation_registry.clear_pending_revocations( - controller=aries_controller, - revocation_registry_credential_map=clear_pending_request.revocation_registry_credential_map, - ) - - bound_logger.debug("Successfully cleared pending revocations.") - return response - - -@router.get( - "/get-pending-revocations/{revocation_registry_id}", - summary="Get Pending Revocations", -) -async def get_pending_revocations( - revocation_registry_id: str, - auth: AcaPyAuth = Depends(acapy_auth_from_header), -) -> PendingRevocations: - """ - Get pending revocations - --- - Get the pending revocations for a given revocation registry ID. - - Parameters: - --- - revocation_registry_id: str - The ID of the revocation registry for which to fetch pending revocations - - Returns: - --- - PendingRevocations: - A list of cred_rev_ids pending revocation for a given revocation registry ID - """ - bound_logger = logger.bind(body={"revocation_registry_id": revocation_registry_id}) - bound_logger.debug("GET request received: Get pending revocations") - - async with client_from_auth(auth) as aries_controller: - bound_logger.debug("Getting pending revocations") - result = await revocation_registry.get_pending_revocations( - controller=aries_controller, rev_reg_id=revocation_registry_id - ) - - bound_logger.debug("Successfully fetched pending revocations.") - return PendingRevocations(pending_cred_rev_ids=result) diff --git a/app/routes/revocation.py b/app/routes/revocation.py new file mode 100644 index 000000000..d6036801f --- /dev/null +++ b/app/routes/revocation.py @@ -0,0 +1,315 @@ +import asyncio +from typing import Optional + +from aries_cloudcontroller import IssuerCredRevRecord +from fastapi import APIRouter, Depends + +from app.dependencies.acapy_clients import client_from_auth +from app.dependencies.auth import AcaPyAuth, acapy_auth_from_header +from app.exceptions import CloudApiException +from app.models.issuer import ( + ClearPendingRevocationsRequest, + ClearPendingRevocationsResult, + PendingRevocations, + PublishRevocationsRequest, + RevokeCredential, + RevokedResponse, +) +from app.services import revocation_registry +from app.util.retry_method import coroutine_with_retry_until_value +from shared.log_config import get_logger + +logger = get_logger(__name__) + +router = APIRouter(prefix="/v1/issuer/credentials", tags=["revocation"]) + + +@router.post("/revoke", summary="Revoke a Credential (if revocable)") +async def revoke_credential( + body: RevokeCredential, + auth: AcaPyAuth = Depends(acapy_auth_from_header), +) -> RevokedResponse: + """ + Revoke a credential + --- + Revoke a credential by providing the identifier of the exchange. + + If an issuer is going to revoke more than one credential, it is recommended to set the + 'auto_publish_on_ledger' field to False (default), and then batch publish the revocations using + the 'publish-revocations' endpoint. + + By batching the revocations, the issuer can save on transaction fees related to + publishing revocations to the ledger. + + Request Body: + --- + body: RevokeCredential + - credential_exchange_id (str): The ID associated with the credential exchange that should be revoked. + - auto_publish_on_ledger (bool): (True) publish revocation to ledger immediately, or + (default, False) mark it pending + + Returns: + --- + RevokedResponse: + revoked_cred_rev_ids: + The revocation registry indexes that were revoked. + Will be empty if the revocation was marked as pending. + """ + bound_logger = logger.bind(body=body) + bound_logger.debug("POST request received: Revoke credential") + + async with client_from_auth(auth) as aries_controller: + bound_logger.debug("Revoking credential") + result = await revocation_registry.revoke_credential( + controller=aries_controller, + credential_exchange_id=body.credential_exchange_id, + auto_publish_to_ledger=body.auto_publish_on_ledger, + ) + + bound_logger.debug("Successfully revoked credential.") + return result + + +@router.get( + "/revocation/record", + summary="Fetch a Revocation Record", + response_model=IssuerCredRevRecord, +) +async def get_credential_revocation_record( + credential_exchange_id: Optional[str] = None, + credential_revocation_id: Optional[str] = None, + revocation_registry_id: Optional[str] = None, + auth: AcaPyAuth = Depends(acapy_auth_from_header), +) -> IssuerCredRevRecord: + """ + Get a credential revocation record + --- + Fetch a credential revocation record by providing the credential exchange id. + Records can also be fetched by providing the credential revocation id and revocation registry id. + + The record is the payload of the webhook event on topic 'issuer_cred_rev', and contains the credential's revocation + status and other metadata. + + The revocation registry id (rev_reg_id) and credential revocation id (cred_rev_id) can be found + in this record if you have the credential exchange id. + + Parameters: + --- + credential_exchange_id: str + credential_revocation_id: str + revocation_registry_id: str + + Returns: + --- + IssuerCredRevRecord + The credential revocation record + + Raises: + --- + CloudApiException: 400 + If credential_exchange_id is not provided, both credential_revocation_id and revocation_registry_id must be. + """ + bound_logger = logger.bind( + body={ + "credential_exchange_id": credential_exchange_id, + "credential_revocation_id": credential_revocation_id, + "revocation_registry_id": revocation_registry_id, + } + ) + bound_logger.debug("GET request received: Get credential revocation record by id") + + if credential_exchange_id is None and ( + credential_revocation_id is None or revocation_registry_id is None + ): + raise CloudApiException( + "If credential_exchange_id is not provided then both " + "credential_revocation_id and revocation_registry_id must be provided.", + 400, + ) + + async with client_from_auth(auth) as aries_controller: + bound_logger.debug("Getting credential revocation record") + revocation_record = await revocation_registry.get_credential_revocation_record( + controller=aries_controller, + credential_exchange_id=credential_exchange_id, + credential_revocation_id=credential_revocation_id, + revocation_registry_id=revocation_registry_id, + ) + + bound_logger.debug("Successfully fetched credential revocation record.") + return revocation_record + + +@router.post("/publish-revocations", summary="Publish Pending Revocations") +async def publish_revocations( + publish_request: PublishRevocationsRequest, + auth: AcaPyAuth = Depends(acapy_auth_from_header), +) -> RevokedResponse: + """ + Write pending revocations to the ledger + --- + Revocations that are in a pending state can be published to the ledger. + + The endpoint accepts a `revocation_registry_credential_map`, which provides a dictionary of + revocation registry IDs to credential revocation IDs, to allow publishing individual credentials. + + If no revocation registry id is provided (i.e. an empty map `revocation_registry_credential_map: {}`), + then all pending revocations will be published. + + If no credential revocation id is provided under a given revocation registry id, then all pending revocations for + the given revocation registry id will be published. + + Where to find the revocation registry id and credential revocation id: + When issuing a credential, against a credential definition that supports revocation, + the issuer will receive a webhook event on the topic 'issuer_cred_rev'. This event will contain + the credential exchange id (cred_ex_id), the credential revocation id (cred_rev_id) and + the revocation registry id (rev_reg_id). + + Request Body: + --- + publish_request: PublishRevocationsRequest + An instance of `PublishRevocationsRequest` containing a `revocation_registry_credential_map`. This map + is a dictionary where each key is a revocation registry ID and its value is a list of credential + revocation IDs to be published. Providing an empty list for a registry ID instructs the system to + publish all pending revocations for that ID. An empty dictionary signifies that all pending + revocations across all registry IDs should be published. + + Returns: + --- + RevokedResponse: + revoked_cred_rev_ids: + The revocation registry indexes that were revoked. + Will be empty if there were no revocations to publish. + """ + bound_logger = logger.bind(body=publish_request) + bound_logger.debug("POST request received: Publish revocations") + + async with client_from_auth(auth) as aries_controller: + bound_logger.debug("Publishing revocations") + result = await revocation_registry.publish_pending_revocations( + controller=aries_controller, + revocation_registry_credential_map=publish_request.revocation_registry_credential_map, + ) + + if not result: + bound_logger.debug("No revocations to publish.") + return RevokedResponse() + + endorser_transaction_ids = [txn.transaction_id for txn in result.txn] + for endorser_transaction_id in endorser_transaction_ids: + bound_logger.debug( + "Wait for publish complete on transaction id: {}", + endorser_transaction_id, + ) + try: + # Wait for transaction to be acknowledged and written to the ledger + await coroutine_with_retry_until_value( + coroutine_func=aries_controller.endorse_transaction.get_transaction, + args=(endorser_transaction_id,), + field_name="state", + expected_value="transaction_acked", + logger=bound_logger, + max_attempts=30, + retry_delay=1, + ) + except asyncio.TimeoutError as e: + raise CloudApiException( + "Timeout waiting for endorser to accept the revocations request.", + 504, + ) from e + + bound_logger.debug("Successfully published revocations.") + return RevokedResponse.model_validate(result.model_dump()) + + +@router.post( + "/clear-pending-revocations", + summary="Clear Pending Revocations", + response_model=ClearPendingRevocationsResult, +) +async def clear_pending_revocations( + clear_pending_request: ClearPendingRevocationsRequest, + auth: AcaPyAuth = Depends(acapy_auth_from_header), +) -> ClearPendingRevocationsResult: + """ + Clear pending revocations + --- + Revocations that are in a pending state can be cleared, such that they are no longer set to be revoked. + + The endpoint accepts a `revocation_registry_credential_map`, which provides a dictionary of + revocation registry IDs to credential revocation IDs, to allow clearing individual credentials. + + If no revocation registry id is provided (i.e. an empty map `revocation_registry_credential_map: {}`), + then all pending revocations will be cleared. + + If no credential revocation id is provided under a given revocation registry id, then all pending revocations for + the given revocation registry id will be cleared. + + Where to find the revocation registry id and credential revocation id: + When issuing a credential, against a credential definition that supports revocation, + the issuer will receive a webhook event on the topic 'issuer_cred_rev'. This event will contain + the credential exchange id (cred_ex_id), the credential revocation id (cred_rev_id) and + the revocation registry id (rev_reg_id). + + Request Body: + --- + clear_pending_request: ClearPendingRevocationsRequest + An instance of `ClearPendingRevocationsRequest` containing a `revocation_registry_credential_map`. This map + is a dictionary where each key is a revocation registry ID and its value is a list of credential + revocation IDs to be cleared. Providing an empty list for a registry ID instructs the system to + clear all pending revocations for that ID. An empty dictionary signifies that all pending + revocations across all registry IDs should be cleared. + + Returns: + --- + ClearPendingRevocationsResult + The revocations that are still pending after the clear request is performed + """ + bound_logger = logger.bind(body=clear_pending_request) + bound_logger.debug("POST request received: Clear pending revocations") + + async with client_from_auth(auth) as aries_controller: + bound_logger.debug("Clearing pending revocations") + response = await revocation_registry.clear_pending_revocations( + controller=aries_controller, + revocation_registry_credential_map=clear_pending_request.revocation_registry_credential_map, + ) + + bound_logger.debug("Successfully cleared pending revocations.") + return response + + +@router.get( + "/get-pending-revocations/{revocation_registry_id}", + summary="Get Pending Revocations", +) +async def get_pending_revocations( + revocation_registry_id: str, + auth: AcaPyAuth = Depends(acapy_auth_from_header), +) -> PendingRevocations: + """ + Get pending revocations + --- + Get the pending revocations for a given revocation registry ID. + + Parameters: + --- + revocation_registry_id: str + The ID of the revocation registry for which to fetch pending revocations + + Returns: + --- + PendingRevocations: + A list of cred_rev_ids pending revocation for a given revocation registry ID + """ + bound_logger = logger.bind(body={"revocation_registry_id": revocation_registry_id}) + bound_logger.debug("GET request received: Get pending revocations") + + async with client_from_auth(auth) as aries_controller: + bound_logger.debug("Getting pending revocations") + result = await revocation_registry.get_pending_revocations( + controller=aries_controller, rev_reg_id=revocation_registry_id + ) + + bound_logger.debug("Successfully fetched pending revocations.") + return PendingRevocations(pending_cred_rev_ids=result) diff --git a/app/tests/e2e/test_revocation.py b/app/tests/e2e/test_revocation.py index 048ea4b26..da9aa4a8e 100644 --- a/app/tests/e2e/test_revocation.py +++ b/app/tests/e2e/test_revocation.py @@ -3,7 +3,7 @@ import pytest from fastapi import HTTPException -from app.routes.issuer import router +from app.routes.revocation import router from app.routes.verifier import router as verifier_router from app.tests.util.regression_testing import TestMode from shared import RichAsyncClient diff --git a/app/tests/fixtures/credentials.py b/app/tests/fixtures/credentials.py index d0f64de4a..e73f13f19 100644 --- a/app/tests/fixtures/credentials.py +++ b/app/tests/fixtures/credentials.py @@ -6,6 +6,7 @@ from pydantic import BaseModel from app.routes.issuer import router +from app.routes.revocation import router as revocation_router from app.routes.wallet.credentials import router as wallets_router from app.tests.util.connections import FaberAliceConnect, MeldCoAliceConnect from app.tests.util.regression_testing import assert_fail_on_recreating_fixtures @@ -15,7 +16,7 @@ CREDENTIALS_BASE_PATH = router.prefix WALLET_BASE_PATH = wallets_router.prefix - +REVOCATION_BASE_PATH = revocation_router.prefix sample_credential_attributes = {"speed": "10", "name": "Alice", "age": "44"} @@ -212,7 +213,7 @@ async def revoke_alice_creds( for cred in issue_alice_creds: await faber_client.post( - f"{CREDENTIALS_BASE_PATH}/revoke", + f"{REVOCATION_BASE_PATH}/revoke", json={ "credential_exchange_id": cred.credential_exchange_id, }, @@ -234,7 +235,7 @@ async def revoke_alice_creds_and_publish( for cred in issue_alice_creds: await faber_client.post( - f"{CREDENTIALS_BASE_PATH}/revoke", + f"{REVOCATION_BASE_PATH}/revoke", json={ "credential_exchange_id": cred.credential_exchange_id, "auto_publish_on_ledger": auto_publish, @@ -244,7 +245,7 @@ async def revoke_alice_creds_and_publish( if not auto_publish: await faber_client.post( - f"{CREDENTIALS_BASE_PATH}/publish-revocations", + f"{REVOCATION_BASE_PATH}/publish-revocations", json={ "revocation_registry_credential_map": {}, }, diff --git a/app/tests/routes/issuer/test_clear_pending_revocations.py b/app/tests/routes/revocation/test_clear_pending_revocations.py similarity index 91% rename from app/tests/routes/issuer/test_clear_pending_revocations.py rename to app/tests/routes/revocation/test_clear_pending_revocations.py index da6551bb8..6de5fc21a 100644 --- a/app/tests/routes/issuer/test_clear_pending_revocations.py +++ b/app/tests/routes/revocation/test_clear_pending_revocations.py @@ -9,7 +9,7 @@ from fastapi import HTTPException from app.models.issuer import ClearPendingRevocationsRequest -from app.routes.issuer import clear_pending_revocations +from app.routes.revocation import clear_pending_revocations @pytest.mark.anyio @@ -17,7 +17,9 @@ 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( + with patch( + "app.routes.revocation.client_from_auth" + ) as mock_client_from_auth, patch( "app.services.revocation_registry.clear_pending_revocations", mock_clear_pending_revocations, ): @@ -56,7 +58,7 @@ async def test_clear_pending_revocations_fail_acapy_error( ) with patch( - "app.routes.issuer.client_from_auth" + "app.routes.revocation.client_from_auth" ) as mock_client_from_auth, pytest.raises( HTTPException, match=expected_detail ) as exc: diff --git a/app/tests/routes/issuer/test_get_credential_revocation_record.py b/app/tests/routes/revocation/test_get_credential_revocation_record.py similarity index 93% rename from app/tests/routes/issuer/test_get_credential_revocation_record.py rename to app/tests/routes/revocation/test_get_credential_revocation_record.py index b0eec7d05..9d9bdc338 100644 --- a/app/tests/routes/issuer/test_get_credential_revocation_record.py +++ b/app/tests/routes/revocation/test_get_credential_revocation_record.py @@ -8,7 +8,7 @@ ) from fastapi import HTTPException -from app.routes.issuer import get_credential_revocation_record +from app.routes.revocation import get_credential_revocation_record @pytest.mark.anyio @@ -22,7 +22,9 @@ async def test_get_credential_revocation_record_success( mock_aries_controller = AsyncMock() mock_get_revocation_record = AsyncMock() - with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + with patch( + "app.routes.revocation.client_from_auth" + ) as mock_client_from_auth, patch( "app.services.revocation_registry.get_credential_revocation_record", mock_get_revocation_record, ): @@ -63,7 +65,7 @@ async def test_get_credential_revocation_record_fail_acapy_error( ) with patch( - "app.routes.issuer.client_from_auth" + "app.routes.revocation.client_from_auth" ) as mock_client_from_auth, pytest.raises( HTTPException, match=expected_detail ) as exc: diff --git a/app/tests/routes/issuer/test_get_pending_revocations.py b/app/tests/routes/revocation/test_get_pending_revocations.py similarity index 90% rename from app/tests/routes/issuer/test_get_pending_revocations.py rename to app/tests/routes/revocation/test_get_pending_revocations.py index 8f5c17fda..6dec60ffd 100644 --- a/app/tests/routes/issuer/test_get_pending_revocations.py +++ b/app/tests/routes/revocation/test_get_pending_revocations.py @@ -8,7 +8,7 @@ ) from fastapi import HTTPException -from app.routes.issuer import get_pending_revocations +from app.routes.revocation import get_pending_revocations rev_reg_id = "mocked_rev_reg_id" @@ -18,7 +18,9 @@ async def test_get_pending_revocations_success(): mock_aries_controller = AsyncMock() mock_get_pending_revocations = AsyncMock(return_value=[1, 2, 3]) - with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + with patch( + "app.routes.revocation.client_from_auth" + ) as mock_client_from_auth, patch( "app.services.revocation_registry.get_pending_revocations", mock_get_pending_revocations, ): @@ -53,7 +55,7 @@ async def test_get_pending_revocations_fail_acapy_error( ) with patch( - "app.routes.issuer.client_from_auth" + "app.routes.revocation.client_from_auth" ) as mock_client_from_auth, pytest.raises( HTTPException, match=expected_detail, diff --git a/app/tests/routes/issuer/test_publish_revocations.py b/app/tests/routes/revocation/test_publish_revocations.py similarity index 89% rename from app/tests/routes/issuer/test_publish_revocations.py rename to app/tests/routes/revocation/test_publish_revocations.py index 2a3f44e15..8033d47ad 100644 --- a/app/tests/routes/issuer/test_publish_revocations.py +++ b/app/tests/routes/revocation/test_publish_revocations.py @@ -6,7 +6,7 @@ from app.exceptions.cloudapi_exception import CloudApiException from app.models.issuer import PublishRevocationsRequest -from app.routes.issuer import publish_revocations +from app.routes.revocation import publish_revocations from app.tests.util.models.dummy_txn_record_publish import txn_record @@ -21,11 +21,13 @@ async def test_publish_revocations_success(publish_revocation_response): mock_get_transaction = AsyncMock() - with patch("app.routes.issuer.client_from_auth") as mock_client_from_auth, patch( + with patch( + "app.routes.revocation.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 + "app.routes.revocation.coroutine_with_retry_until_value", mock_get_transaction ): mock_client_from_auth.return_value.__aenter__.return_value = ( mock_aries_controller @@ -64,7 +66,7 @@ async def test_publish_revocations_fail_acapy_error( ) with patch( - "app.routes.issuer.client_from_auth" + "app.routes.revocation.client_from_auth" ) as mock_client_from_auth, pytest.raises( CloudApiException, match=expected_detail ) as exc, patch( @@ -92,7 +94,7 @@ async def test_publish_revocations_fail_timeout(): ) with patch( - "app.routes.issuer.client_from_auth" + "app.routes.revocation.client_from_auth" ) as mock_client_from_auth, pytest.raises( CloudApiException, match="Timeout waiting for endorser to accept the revocations request.", @@ -100,7 +102,7 @@ async def test_publish_revocations_fail_timeout(): "app.services.revocation_registry.publish_pending_revocations", mock_publish_revocations, ), patch( - "app.routes.issuer.coroutine_with_retry_until_value", + "app.routes.revocation.coroutine_with_retry_until_value", AsyncMock(side_effect=asyncio.TimeoutError()), ): mock_client_from_auth.return_value.__aenter__.return_value = ( diff --git a/app/tests/routes/issuer/test_revoke_credential.py b/app/tests/routes/revocation/test_revoke_credential.py similarity index 92% rename from app/tests/routes/issuer/test_revoke_credential.py rename to app/tests/routes/revocation/test_revoke_credential.py index 9ba92f8d2..f639900f1 100644 --- a/app/tests/routes/issuer/test_revoke_credential.py +++ b/app/tests/routes/revocation/test_revoke_credential.py @@ -4,7 +4,7 @@ from app.exceptions.cloudapi_exception import CloudApiException from app.models.issuer import RevokeCredential -from app.routes.issuer import revoke_credential +from app.routes.revocation import revoke_credential credential_exchange_id = "v2-db9d7025-b276-4c32-ae38-fbad41864112" @@ -14,7 +14,9 @@ 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( + with patch( + "app.routes.revocation.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 = ( @@ -55,7 +57,7 @@ async def test_revoke_credential_fail_acapy_error( ) with patch( - "app.routes.issuer.client_from_auth" + "app.routes.revocation.client_from_auth" ) as mock_client_from_auth, pytest.raises( CloudApiException, match=expected_detail ) as exc, patch( diff --git a/app/tests/services/issuer/test_issuer.py b/app/tests/services/issuer/test_issuer.py index 88204dd19..628d73aa4 100644 --- a/app/tests/services/issuer/test_issuer.py +++ b/app/tests/services/issuer/test_issuer.py @@ -339,31 +339,3 @@ async def test_create_offer( verify(IssuerV2).create_offer( controller=mock_agent_controller, credential=v2_credential ) - - -@pytest.mark.anyio -async def test_revoke_credential( - mock_agent_controller: AcaPyClient, - mock_context_managed_controller: MockContextManagedController, - mock_tenant_auth: AcaPyAuth, - mocker: MockerFixture, -): - mocker.patch.object( - test_module, - "client_from_auth", - return_value=mock_context_managed_controller(mock_agent_controller), - ) - - revoke_credential = mock(RevokeCredential) - revoke_credential.credential_exchange_id = "random_cred_ex_id" - revoke_credential.auto_publish_on_ledger = True - status_code = 204 - - when(revocation_registry).revoke_credential(...).thenReturn(to_async(status_code)) - await test_module.revoke_credential(body=revoke_credential, auth=mock_tenant_auth) - - verify(revocation_registry).revoke_credential( - controller=mock_agent_controller, - credential_exchange_id=revoke_credential.credential_exchange_id, - auto_publish_to_ledger=revoke_credential.auto_publish_on_ledger, - )