Skip to content

Commit

Permalink
Merge pull request openwallet-foundation#1866 from sicpa-dlab/feature…
Browse files Browse the repository at this point in the history
…/universal-resolver

feat: add universal resolver
  • Loading branch information
swcurran authored Jul 21, 2022
2 parents 2fb70f3 + ff2f106 commit 4b1fa13
Show file tree
Hide file tree
Showing 5 changed files with 409 additions and 0 deletions.
34 changes: 34 additions & 0 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,28 @@ def add_arguments(self, parser: ArgumentParser):
env_var="ACAPY_READ_ONLY_LEDGER",
help="Sets ledger to read-only to prevent updates. Default: false.",
)
parser.add_argument(
"--universal-resolver",
type=str,
nargs="?",
metavar="<universal_resolver_endpoint>",
env_var="ACAPY_UNIVERSAL_RESOLVER",
const="DEFAULT",
help="Enable resolution from a universal resolver.",
)
parser.add_argument(
"--universal-resolver-regex",
type=str,
nargs="+",
metavar="<did_regex>",
env_var="ACAPY_UNIVERSAL_RESOLVER_REGEX",
help=(
"Regex matching DIDs to resolve using the unversal resolver. "
"Multiple can be specified. "
"Defaults to a regex matching all DIDs resolvable by universal "
"resolver instance."
),
)

def get_settings(self, args: Namespace) -> dict:
"""Extract general settings."""
Expand Down Expand Up @@ -659,6 +681,18 @@ def get_settings(self, args: Namespace) -> dict:

if args.read_only_ledger:
settings["read_only_ledger"] = True

if args.universal_resolver_regex and not args.universal_resolver:
raise ArgsParseError(
"--universal-resolver-regex cannot be used without --universal-resolver"
)

if args.universal_resolver:
settings["resolver.universal"] = args.universal_resolver

if args.universal_resolver_regex:
settings["resolver.universal.supported"] = args.universal_resolver_regex

return settings


Expand Down
44 changes: 44 additions & 0 deletions aries_cloudagent/config/tests/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,3 +469,47 @@ async def test_discover_features_args(self):
assert (["test_goal_code_1", "test_goal_code_2"]) == settings.get(
"disclose_goal_code_list"
)

def test_universal_resolver(self):
"""Test universal resolver flags."""
parser = argparse.create_argument_parser()
group = argparse.GeneralGroup()
group.add_arguments(parser)

result = parser.parse_args(["-e", "test", "--universal-resolver"])
settings = group.get_settings(result)
endpoint = settings.get("resolver.universal")
assert endpoint
assert endpoint == "DEFAULT"

result = parser.parse_args(
["-e", "test", "--universal-resolver", "https://example.com"]
)
settings = group.get_settings(result)
endpoint = settings.get("resolver.universal")
assert endpoint
assert endpoint == "https://example.com"

result = parser.parse_args(
[
"-e",
"test",
"--universal-resolver",
"https://example.com",
"--universal-resolver-regex",
"regex",
]
)
settings = group.get_settings(result)
endpoint = settings.get("resolver.universal")
assert endpoint
assert endpoint == "https://example.com"
supported_regex = settings.get("resolver.universal.supported")
assert supported_regex
assert supported_regex == ["regex"]

result = parser.parse_args(
["-e", "test", "--universal-resolver-regex", "regex"]
)
with self.assertRaises(argparse.ArgsParseError):
group.get_settings(result)
7 changes: 7 additions & 0 deletions aries_cloudagent/resolver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,10 @@ async def setup(context: InjectionContext):
).provide(context.settings, context.injector)
await web_resolver.setup(context)
registry.register(web_resolver)

if context.settings.get("resolver.universal"):
universal_resolver = ClassProvider(
"aries_cloudagent.resolver.default.universal.UniversalResolver"
).provide(context.settings, context.injector)
await universal_resolver.setup(context)
registry.register(universal_resolver)
222 changes: 222 additions & 0 deletions aries_cloudagent/resolver/default/tests/test_universal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
"""Test universal resolver with http bindings."""

import re
from typing import Dict, Union

from asynctest import mock as async_mock
import pytest

from aries_cloudagent.config.settings import Settings

from .. import universal as test_module
from ...base import DIDNotFound, ResolverError
from ..universal import UniversalResolver


@pytest.fixture
async def resolver():
"""Resolver fixture."""
yield UniversalResolver(
endpoint="https://example.com", supported_did_regex=re.compile("^did:sov:.*$")
)


@pytest.fixture
def profile():
"""Profile fixture."""
yield async_mock.MagicMock()


class MockResponse:
"""Mock http response."""

def __init__(self, status: int, body: Union[str, Dict]):
self.status = status
self.body = body

async def json(self):
return self.body

async def text(self):
return self.body

async def __aenter__(self):
"""For use as async context."""
return self

async def __aexit__(self, err_type, err_value, err_exc):
"""For use as async context."""


class MockClientSession:
"""Mock client session."""

def __init__(self, response: MockResponse = None):
self.response = response

def __call__(self):
return self

async def __aenter__(self):
"""For use as async context."""
return self

async def __aexit__(self, err_type, err_value, err_exc):
"""For use as async context."""

def get(self, endpoint):
"""Return response."""
return self.response


@pytest.fixture
def mock_client_session():
temp = test_module.aiohttp.ClientSession
session = MockClientSession()
test_module.aiohttp.ClientSession = session
yield session
test_module.aiohttp.ClientSession = temp


@pytest.mark.asyncio
async def test_resolve(profile, resolver, mock_client_session):
mock_client_session.response = MockResponse(
200,
{
"didDocument": {
"id": "did:example:123",
"@context": "https://www.w3.org/ns/did/v1",
}
},
)
doc = await resolver.resolve(profile, "did:sov:WRfXPg8dantKVubE3HX8pw")
assert doc.get("id") == "did:example:123"


@pytest.mark.asyncio
async def test_resolve_not_found(profile, resolver, mock_client_session):
mock_client_session.response = MockResponse(404, "Not found")
with pytest.raises(DIDNotFound):
await resolver.resolve(profile, "did:sov:WRfXPg8dantKVubE3HX8pw")


@pytest.mark.asyncio
async def test_resolve_unexpeceted_status(profile, resolver, mock_client_session):
mock_client_session.response = MockResponse(
500, "Server failed to complete request"
)
with pytest.raises(ResolverError):
await resolver.resolve(profile, "did:sov:WRfXPg8dantKVubE3HX8pw")


@pytest.mark.asyncio
async def test_fetch_resolver_props(mock_client_session: MockClientSession):
mock_client_session.response = MockResponse(200, {"test": "json"})
assert await test_module._fetch_resolver_props("test") == {"test": "json"}
mock_client_session.response = MockResponse(404, "Not found")
with pytest.raises(ResolverError):
await test_module._fetch_resolver_props("test")


@pytest.mark.asyncio
async def test_get_supported_did_regex():
props = {"example": {"http": {"pattern": "match a test string"}}}
with async_mock.patch.object(
test_module,
"_fetch_resolver_props",
async_mock.CoroutineMock(return_value=props),
):
pattern = await test_module._get_supported_did_regex("test")
assert pattern.fullmatch("match a test string")


def test_compile_supported_did_regex():
patterns = ["one", "two", "three"]
compiled = test_module._compile_supported_did_regex(patterns)
assert compiled.match("one")
assert compiled.match("two")
assert compiled.match("three")


@pytest.mark.asyncio
async def test_setup_endpoint_regex_set(resolver: UniversalResolver):
settings = Settings(
{
"resolver.universal": "http://example.com",
"resolver.universal.supported": "test",
}
)
context = async_mock.MagicMock()
context.settings = settings
with async_mock.patch.object(
test_module,
"_compile_supported_did_regex",
async_mock.MagicMock(return_value="pattern"),
):
await resolver.setup(context)

assert resolver._endpoint == "http://example.com"
assert resolver._supported_did_regex == "pattern"


@pytest.mark.asyncio
async def test_setup_endpoint_set(resolver: UniversalResolver):
settings = Settings(
{
"resolver.universal": "http://example.com",
}
)
context = async_mock.MagicMock()
context.settings = settings
with async_mock.patch.object(
test_module,
"_get_supported_did_regex",
async_mock.CoroutineMock(return_value="pattern"),
):
await resolver.setup(context)

assert resolver._endpoint == "http://example.com"
assert resolver._supported_did_regex == "pattern"


@pytest.mark.asyncio
async def test_setup_endpoint_default(resolver: UniversalResolver):
settings = Settings(
{
"resolver.universal": "DEFAULT",
}
)
context = async_mock.MagicMock()
context.settings = settings
with async_mock.patch.object(
test_module,
"_get_supported_did_regex",
async_mock.CoroutineMock(return_value="pattern"),
):
await resolver.setup(context)

assert resolver._endpoint == test_module.DEFAULT_ENDPOINT
assert resolver._supported_did_regex == "pattern"


@pytest.mark.asyncio
async def test_setup_endpoint_unset(resolver: UniversalResolver):
settings = Settings()
context = async_mock.MagicMock()
context.settings = settings
with async_mock.patch.object(
test_module,
"_get_supported_did_regex",
async_mock.CoroutineMock(return_value="pattern"),
):
await resolver.setup(context)

assert resolver._endpoint == test_module.DEFAULT_ENDPOINT
assert resolver._supported_did_regex == "pattern"


@pytest.mark.asyncio
async def test_supported_did_regex_not_setup():
resolver = UniversalResolver()
with pytest.raises(ResolverError):
resolver.supported_did_regex
Loading

0 comments on commit 4b1fa13

Please sign in to comment.