Skip to content

Commit

Permalink
✨ Add credentials routes to wallet module (#484)
Browse files Browse the repository at this point in the history
* empty init

* 🚚 move and rename module

* ✨ Added credentials routes to wallet module

* 🎨 rename wallet import

* 🎨 rename test module

* 🎨 rename listener

* ✅ add tests for wallet credentials route

* 🎨 isort

* empty init

* 🚚 move and rename module

* ✨ Added credentials routes to wallet module

* 🎨 rename wallet import

* 🎨 rename test module

* 🎨 rename listener

* ✅ add tests for wallet credentials route

* 🎨 isort
  • Loading branch information
ff137 authored Oct 6, 2023
1 parent 103d59a commit d5c3af0
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 10 deletions.
6 changes: 4 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@
sse,
trust_registry,
verifier,
wallet,
webhooks,
)
from app.routes.admin import tenants
from app.routes.wallet import credentials as wallet_credentials
from app.routes.wallet import dids as wallet_dids
from shared.log_config import get_logger

OPENAPI_NAME = os.getenv("OPENAPI_NAME", "OpenAPI")
Expand All @@ -47,7 +48,8 @@ def create_app() -> FastAPI:
oob,
trust_registry,
verifier,
wallet,
wallet_credentials,
wallet_dids,
webhooks,
sse,
]
Expand Down
Empty file added app/routes/wallet/__init__.py
Empty file.
181 changes: 181 additions & 0 deletions app/routes/wallet/credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
from typing import Optional

from aries_cloudcontroller import (
AttributeMimeTypesResult,
CredInfoList,
CredRevokedResult,
IndyCredInfo,
VCRecord,
VCRecordList,
W3CCredentialsListRequest,
)
from fastapi import APIRouter, Depends

from app.dependencies.acapy_clients import client_from_auth
from app.dependencies.auth import AcaPyAuth, acapy_auth
from shared.log_config import get_logger

logger = get_logger(__name__)

router = APIRouter(prefix="/wallet/credentials", tags=["wallet"])


@router.get("", response_model=CredInfoList)
async def list_credentials(
count: Optional[str] = None,
start: Optional[str] = None,
wql: Optional[str] = None,
auth: AcaPyAuth = Depends(acapy_auth),
):
"""Fetch a list of credentials from the wallet."""
logger.info("GET request received: List credentials")

async with client_from_auth(auth) as aries_controller:
logger.debug("Fetching credentials")
results = await aries_controller.credentials.get_records(
count=count, start=start, wql=wql
)

logger.info("Successfully listed credentials.")
return results


@router.get("/{credential_id}", response_model=IndyCredInfo)
async def get_credential_record(
credential_id: str,
auth: AcaPyAuth = Depends(acapy_auth),
):
"""Fetch a specific credential by ID."""
bound_logger = logger.bind(credential_id=credential_id)
bound_logger.info("GET request received: Fetch specific credential by ID")

async with client_from_auth(auth) as aries_controller:
bound_logger.debug("Fetching credential")
result = await aries_controller.credentials.get_record(
credential_id=credential_id
)

bound_logger.info("Successfully fetched credential.")
return result


@router.delete("/{credential_id}", status_code=204)
async def delete_credential(
credential_id: str,
auth: AcaPyAuth = Depends(acapy_auth),
):
"""Remove a specific credential from the wallet by ID."""
bound_logger = logger.bind(credential_id=credential_id)
bound_logger.info("DELETE request received: Remove specific credential by ID")

async with client_from_auth(auth) as aries_controller:
bound_logger.debug("Deleting credential")
result = await aries_controller.credentials.delete_record(
credential_id=credential_id
)

bound_logger.info("Successfully deleted credential.")
return result


@router.get("/{credential_id}/mime-types", response_model=AttributeMimeTypesResult)
async def get_credential_mime_types(
credential_id: str,
auth: AcaPyAuth = Depends(acapy_auth),
):
"""Retrieve attribute MIME types of a specific credential by ID."""
bound_logger = logger.bind(credential_id=credential_id)
bound_logger.info(
"GET request received: Retrieve attribute MIME types for a specific credential"
)

async with client_from_auth(auth) as aries_controller:
bound_logger.debug("Fetching MIME types")
result = await aries_controller.credentials.get_credential_mime_types(
credential_id=credential_id
)

bound_logger.info("Successfully fetched attribute MIME types.")
return result


@router.get("/{credential_id}/revocation-status", response_model=CredRevokedResult)
async def get_credential_revocation_status(
credential_id: str,
from_: Optional[str] = None,
to: Optional[str] = None,
auth: AcaPyAuth = Depends(acapy_auth),
):
"""Query the revocation status of a specific credential by ID."""
bound_logger = logger.bind(credential_id=credential_id)
bound_logger.info(
"GET request received: Query revocation status for a specific credential"
)

async with client_from_auth(auth) as aries_controller:
bound_logger.debug("Fetching revocation status")
result = await aries_controller.credentials.get_revocation_status(
credential_id=credential_id, from_=from_, to=to
)

bound_logger.info("Successfully fetched revocation status.")
return result


@router.get("/w3c", response_model=VCRecordList)
async def list_w3c_credentials(
count: Optional[str] = None,
start: Optional[str] = None,
wql: Optional[str] = None,
body: Optional[W3CCredentialsListRequest] = None,
auth: AcaPyAuth = Depends(acapy_auth),
):
"""Fetch a list of W3C credentials from the wallet."""
logger.info("GET request received: List W3C credentials")

async with client_from_auth(auth) as aries_controller:
logger.debug("Fetching W3C credentials")
results = await aries_controller.credentials.get_w3c_credentials(
count=count, start=start, wql=wql, body=body
)

logger.info("Successfully listed W3C credentials.")
return results


@router.get("/w3c/{credential_id}", response_model=VCRecord)
async def get_w3c_credential(
credential_id: str,
auth: AcaPyAuth = Depends(acapy_auth),
):
"""Fetch a specific W3C credential by ID."""
bound_logger = logger.bind(credential_id=credential_id)
bound_logger.info("GET request received: Fetch specific W3C credential by ID")

async with client_from_auth(auth) as aries_controller:
bound_logger.debug("Fetching W3C credential")
result = await aries_controller.credentials.get_w3c_credential(
credential_id=credential_id
)

bound_logger.info("Successfully fetched W3C credential.")
return result


@router.delete("/w3c/{credential_id}", status_code=204)
async def delete_w3c_credential(
credential_id: str,
auth: AcaPyAuth = Depends(acapy_auth),
):
"""Remove a specific W3C credential from the wallet by ID."""
bound_logger = logger.bind(credential_id=credential_id)
bound_logger.info("DELETE request received: Remove specific W3C credential by ID")

async with client_from_auth(auth) as aries_controller:
bound_logger.debug("Deleting W3C credential")
result = await aries_controller.credentials.delete_w3c_credential(
credential_id=credential_id
)

bound_logger.info("Successfully deleted W3C credential.")
return result
File renamed without changes.
2 changes: 1 addition & 1 deletion app/tests/e2e/issuer/did_key_ed/test_ld_ed25519.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from app.routes.connections import router as con_router
from app.routes.issuer import router as issuer_router
from app.routes.oob import router as oob_router
from app.routes.wallet import router as wallet_router
from app.routes.wallet.dids import router as wallet_router
from app.tests.util.ecosystem_connections import FaberAliceConnect
from app.tests.util.trust_registry import DidKey
from app.tests.util.webhooks import check_webhook_state
Expand Down
2 changes: 1 addition & 1 deletion app/tests/e2e/issuer/did_sov/test_v2_ld.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from app.models.issuer import SendCredential
from app.routes.issuer import router as issuer_router
from app.routes.oob import router as oob_router
from app.routes.wallet import router as wallet_router
from app.routes.wallet.dids import router as wallet_router
from app.tests.util.ecosystem_connections import FaberAliceConnect
from app.tests.util.webhooks import check_webhook_state
from shared import RichAsyncClient
Expand Down
8 changes: 5 additions & 3 deletions app/tests/e2e/test_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,17 @@ async def issue_credential_to_alice(
},
}

listener = SseListener(topic="credentials", wallet_id=alice_tenant.tenant_id)
alice_credentials_listener = SseListener(
topic="credentials", wallet_id=alice_tenant.tenant_id
)

# create and send credential offer- issuer
await faber_client.post(
CREDENTIALS_BASE_PATH,
json=credential,
)

payload = await listener.wait_for_event(
payload = await alice_credentials_listener.wait_for_event(
field="connection_id",
field_id=faber_and_alice_connection.alice_connection_id,
desired_state="offer-received",
Expand All @@ -166,7 +168,7 @@ async def issue_credential_to_alice(
f"{CREDENTIALS_BASE_PATH}/{alice_credential_id}/request", json={}
)

await listener.wait_for_event(
await alice_credentials_listener.wait_for_event(
field="credential_id", field_id=alice_credential_id, desired_state="done"
)

Expand Down
65 changes: 65 additions & 0 deletions app/tests/e2e/test_wallet_credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import logging

import pytest
from fastapi import HTTPException

from app.routes.wallet.credentials import router
from shared import RichAsyncClient
from shared.models.topics.base import CredentialExchange

WALLET_CREDENTIALS_PATH = router.prefix

logger = logging.getLogger(__name__)


@pytest.mark.anyio
async def test_get_credentials(alice_member_client: RichAsyncClient):
# Assert empty list is returned for empty wallet when fetching all credentials
response = await alice_member_client.get(WALLET_CREDENTIALS_PATH)

assert response.status_code == 200
response = response.json()
logger.info("response: %s", response)
assert response == {"results": []}


@pytest.mark.anyio
async def test_get_and_delete_credential_record(
alice_member_client: RichAsyncClient, issue_credential_to_alice: CredentialExchange
):
credentials_response = await alice_member_client.get(WALLET_CREDENTIALS_PATH)

assert credentials_response.status_code == 200
credentials_response = credentials_response.json()["results"]

assert len(credentials_response) == 1

credential_id = credentials_response[0]["referent"]
# While in the broader context of Aries and credentials, referent can refer to specific attributes,
# when dealing with the wallet's stored credentials, the referent becomes synonymous with a credential_id
# specific to the wallet. It's how the wallet references and retrieves that particular credential record.

fetch_response = await alice_member_client.get(
f"{WALLET_CREDENTIALS_PATH}/{credential_id}"
)
assert fetch_response.status_code == 200
fetch_response = fetch_response.json()
logger.info("fetch_response: %s", fetch_response)

# Assert we can delete this credential
delete_response = await alice_member_client.delete(
f"{WALLET_CREDENTIALS_PATH}/{credential_id}"
)
assert delete_response.status_code == 204

# Assert credential list is now empty
credentials_response = await alice_member_client.get(WALLET_CREDENTIALS_PATH)
assert credentials_response.status_code == 200
assert credentials_response.json() == {"results": []}

# Assert fetching deleted credential yields 404
with pytest.raises(HTTPException) as exc:
credentials_response = await alice_member_client.get(
f"{WALLET_CREDENTIALS_PATH}/{credential_id}"
)
assert exc.value.status_code == 404
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import app.services.acapy_wallet as wallet_facade
from app.dependencies.auth import AcaPyAuthVerified
from app.models.wallet import SetDidEndpointRequest
from app.routes.wallet import (
from app.routes.wallet.dids import (
get_did_endpoint,
get_public_did,
list_dids,
Expand Down
4 changes: 2 additions & 2 deletions app/tests/util/trust_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from app.routes.wallet import router
from app.routes.wallet.dids import router as wallet_router
from app.services.trust_registry import (
Actor,
actor_by_did,
Expand All @@ -15,7 +15,7 @@
)
from shared import RichAsyncClient

WALLET_BASE_PATH = router.prefix
WALLET_BASE_PATH = wallet_router.prefix


async def register_issuer(issuer_client: RichAsyncClient, schema_id: str):
Expand Down

0 comments on commit d5c3af0

Please sign in to comment.