Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add universal resolver #1866

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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