forked from openwallet-foundation/acapy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request openwallet-foundation#27 from burdettadam/feature/…
…resolver-admin-api feature/resolver-admin-api
- Loading branch information
Showing
7 changed files
with
260 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Interfaces and base classes for DID Resolution.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
""" | ||
Resolve did document admin routes. | ||
"/resolver/resolve/{did}": { | ||
"get": { | ||
"responses": { | ||
"200": { | ||
"schema": { | ||
"$ref": "#/definitions/DIDDoc" | ||
}, | ||
"description": null | ||
} | ||
}, | ||
"parameters": [ | ||
{ | ||
"in": "path", | ||
"name": "did", | ||
"required": true, | ||
"type": "string", | ||
"pattern": "did:([a-z]+):((?:[a-zA-Z0-9._-]*:)*[a-zA-Z0-9._-]+)", | ||
"description": "decentralize identifier(DID)", | ||
"example": "did:ted:WgWxqztrNooG92RXvxSTWv" | ||
} | ||
], | ||
"tags": [ "resolver" ], | ||
"summary": "Retrieve doc for requested did", | ||
"produces": [ "application/json" ] | ||
} | ||
} | ||
""" | ||
|
||
import re | ||
from aiohttp import web | ||
from aiohttp_apispec import ( | ||
docs, | ||
match_info_schema, | ||
response_schema, | ||
) | ||
from marshmallow import fields, validate | ||
|
||
from ..admin.request_context import AdminRequestContext | ||
from ..messaging.models.base import BaseModelError | ||
from ..messaging.models.openapi import OpenAPISchema | ||
from ..storage.error import StorageError, StorageNotFoundError | ||
from .did_resolver import DIDResolver | ||
from .did import DID_PATTERN | ||
|
||
class W3cDIDDoc(validate.Regexp): | ||
"""Validate value against w3c DID document.""" | ||
|
||
EXAMPLE = "*" | ||
PATTERN = DID_PATTERN | ||
|
||
def __init__(self): | ||
"""Initializer.""" | ||
|
||
super().__init__( | ||
W3cDIDDoc.PATTERN, | ||
error="Value {input} is not a w3c decentralized identifier (DID) doc.", | ||
) | ||
|
||
|
||
_W3cDIDDoc = {"validate": W3cDIDDoc(), "example": W3cDIDDoc.EXAMPLE} | ||
|
||
|
||
class DIDDocSchema(OpenAPISchema): | ||
"""Result schema for did document query.""" | ||
|
||
did_doc = fields.Str( | ||
description="decentralize identifier(DID) document", required=True, **_W3cDIDDoc | ||
) | ||
|
||
|
||
class W3cDID(validate.Regexp): | ||
"""Validate value against w3c DID.""" | ||
|
||
EXAMPLE = "did:ted:WgWxqztrNooG92RXvxSTWv" | ||
PATTERN = re.compile(r"did:([a-z]+):((?:[a-zA-Z0-9._-]*:)*[a-zA-Z0-9._-]+)") | ||
|
||
def __init__(self): | ||
"""Initializer.""" | ||
|
||
super().__init__( | ||
W3cDID.PATTERN, | ||
error="Value {input} is not a w3c decentralized identifier (DID)", | ||
) | ||
|
||
|
||
_W3cDID = {"validate": W3cDID(), "example": W3cDID.EXAMPLE} | ||
|
||
|
||
class DIDMatchInfoSchema(OpenAPISchema): | ||
"""Path parameters and validators for request taking DID.""" | ||
|
||
did = fields.Str( | ||
description="decentralize identifier(DID)", required=True, **_W3cDID | ||
) | ||
|
||
|
||
@docs(tags=["resolver"], summary="Retrieve doc for requested did") | ||
@match_info_schema(DIDMatchInfoSchema()) | ||
@response_schema(DIDDocSchema(), 200) | ||
async def resolve_did(request: web.BaseRequest): | ||
"""Retrieve a did document.""" | ||
context: AdminRequestContext = request["context"] | ||
|
||
did = request.match_info["did"] | ||
try: | ||
session = await context.session() | ||
resolver = session.inject(DIDResolver) | ||
document = await resolver.resolve(context.profile, did) | ||
result = document.serialize() | ||
except StorageNotFoundError as err: | ||
raise web.HTTPNotFound(reason=err.roll_up) from err | ||
except (BaseModelError, StorageError) as err: | ||
raise web.HTTPBadRequest(reason=err.roll_up) from err | ||
return web.json_response(result) | ||
|
||
|
||
async def register(app: web.Application): | ||
"""Register routes.""" | ||
|
||
app.add_routes( | ||
[ | ||
web.get( | ||
"/resolver/resolve/{did}", | ||
resolve_did, | ||
allow_head=False, | ||
), | ||
] | ||
) | ||
|
||
|
||
def post_process_routes(app: web.Application): | ||
"""Amend swagger API.""" | ||
|
||
# Add top-level tags description | ||
if "tags" not in app._state["swagger_dict"]: | ||
app._state["swagger_dict"]["tags"] = [] | ||
app._state["swagger_dict"]["tags"].append( | ||
{ | ||
"name": "resolver", | ||
"description": "universal resolvers", | ||
"externalDocs": {"description": "Specification"}, # , "url": SPEC_URI}, | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import unittest | ||
import asyncio | ||
import pytest | ||
from asynctest import mock as async_mock | ||
from ...storage.error import StorageError, StorageNotFoundError | ||
from ...messaging.models.base import BaseModelError | ||
|
||
from ..base import ( | ||
BaseDIDResolver, | ||
DIDMethodNotSupported, | ||
DIDNotFound, | ||
ResolverType, | ||
) | ||
from ...resolver.did import DID | ||
from ...resolver.diddoc import ResolvedDIDDoc | ||
from ..did_resolver import DIDResolver | ||
from ..did_resolver_registry import DIDResolverRegistry | ||
from .test_did_resolver import TEST_DID_METHODS | ||
from ...admin.request_context import AdminRequestContext | ||
from .test_diddoc import DOC | ||
from .. import routes as test_module | ||
from ...core.in_memory import InMemoryProfile | ||
|
||
did_doc = ResolvedDIDDoc(DOC) | ||
|
||
|
||
@pytest.fixture | ||
def mock_response(): | ||
json_response = async_mock.MagicMock() | ||
temp_value = test_module.web.json_response | ||
test_module.web.json_response = json_response | ||
yield json_response | ||
test_module.web.json_response = temp_value | ||
|
||
|
||
@pytest.fixture | ||
def mock_resolver(): | ||
did_resolver = async_mock.MagicMock() | ||
did_resolver.resolve = async_mock.CoroutineMock(return_value=did_doc) | ||
yield did_resolver | ||
|
||
|
||
@pytest.fixture | ||
def mock_request(mock_resolver): | ||
context = AdminRequestContext.test_context({DIDResolver: mock_resolver}) | ||
outbound_message_router = async_mock.CoroutineMock() | ||
request_dict = { | ||
"context": context, | ||
"outbound_message_router": outbound_message_router, | ||
} | ||
request = async_mock.MagicMock( | ||
match_info={ | ||
"did": "did:ethr:mainnet:0xb9c5714089478a327f09197987f16f9e5d936e8a", | ||
}, | ||
query={}, | ||
json=async_mock.CoroutineMock(return_value={}), | ||
__getitem__=lambda _, k: request_dict[k], | ||
) | ||
yield request | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_resolver(mock_request, mock_response): | ||
await test_module.resolve_did(mock_request) | ||
mock_response.assert_called_once_with(did_doc.serialize()) | ||
# TODO: test http response codes | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_resolver_not_found_error(mock_resolver, mock_request, mock_response): | ||
mock_resolver.resolve = async_mock.CoroutineMock(side_effect=StorageNotFoundError()) | ||
with pytest.raises(test_module.web.HTTPNotFound): | ||
await test_module.resolve_did(mock_request) | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_resolver_bad_req_error(mock_resolver, mock_request, mock_response): | ||
mock_resolver.resolve = async_mock.CoroutineMock(side_effect=BaseModelError()) | ||
with pytest.raises(test_module.web.HTTPBadRequest): | ||
await test_module.resolve_did(mock_request) | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_resolver_bad_req_storage_error( | ||
mock_resolver, mock_request, mock_response | ||
): | ||
mock_resolver.resolve = async_mock.CoroutineMock(side_effect=StorageError()) | ||
with pytest.raises(test_module.web.HTTPBadRequest): | ||
await test_module.resolve_did(mock_request) | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_register(): | ||
mock_app = async_mock.MagicMock() | ||
mock_app.add_routes = async_mock.MagicMock() | ||
await test_module.register(mock_app) | ||
mock_app.add_routes.assert_called_once() | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_post_process_routes(): | ||
mock_app = async_mock.MagicMock(_state={"swagger_dict": {}}) | ||
test_module.post_process_routes(mock_app) | ||
assert "tags" in mock_app._state["swagger_dict"] |