diff --git a/docs/maintainer_features.rst b/docs/maintainer_features.rst index 6821380..1210d97 100644 --- a/docs/maintainer_features.rst +++ b/docs/maintainer_features.rst @@ -76,11 +76,11 @@ Feature Inventory * - Registry requests - ``client.registries()`` - ``signify.app.credentialing`` - - Maintained for registry inception, serialization, readback, and rename. + - Maintained for registry list/read plus canonical parity write APIs, with explicit compatibility aliases for older SignifyPy callers. * - Schema requests - - ``client.oobis()`` - - ``signify.app.coring`` - - Supported as a workflow through schema OOBIs; there is not yet a dedicated ``schemas()`` wrapper. + - ``client.schemas()``, ``client.oobis()`` + - ``signify.app.schemas``, ``signify.app.coring`` + - Maintained as both a dedicated schema read wrapper and an OOBI-backed schema resolution workflow. * - Challenge requests - ``client.challenges()`` - ``signify.app.challenging`` @@ -103,7 +103,7 @@ Feature Inventory - Maintained for multisig request inspection, request fan-out, and join submission. * - Exchange requests - ``client.exchanges()`` - - ``signify.peer.exchanging`` + - ``signify.app.exchanging`` with ``signify.peer.exchanging`` as compatibility spine - Maintained for peer ``exn`` creation, send, fetch, and recipient-specific fan-out. * - OOBI and endpoint publication - ``client.oobis()``, ``client.endroles()``, ``client.identifiers().addEndRole()``, ``addLocScheme()`` @@ -185,8 +185,22 @@ Registry Requests Implementation: ``signify.app.credentialing.Registries`` +Implemented surface today: + +- ``client.registries().list(name)`` +- ``client.registries().get(name, registryName)`` +- ``client.registries().create(name, registryName, *, noBackers=True, baks=None, toad=0, nonce=None)`` +- ``client.registries().createFromEvents(hab, name, registryName, vcp, ixn, sigs)`` +- ``client.registries().rename(name, registryName, newName)`` +- Compatibility aliases remain callable: + + - ``create(hab, registryName, ...)`` + - ``create_from_events(hab, registryName, vcp, ixn, sigs)`` + - ``rename(hab, registryName, newName)`` + Routes: +- ``GET /identifiers/{name}/registries`` - ``GET /identifiers/{name}/registries/{registryName}`` - ``POST /identifiers/{name}/registries`` - ``PUT /identifiers/{name}/registries/{registryName}`` @@ -195,6 +209,11 @@ Responsibilities: - Construct registry inception events and their anchoring interactions. - Submit registry creation from locally built events. +- Keep SignifyTS workflow behavior while preferring the established + KERIpy/KERIA/SignifyPy camelCase idiom for the maintained public write + surface. +- Return ``RegistryResult`` from canonical registry creation paths, with + synchronous ``op()`` unwrapping for the operation payload. - Serialize issuance anchor attachments for downstream IPEX grant workflows. - Rename and re-read registries as part of the maintained read-path contract. @@ -210,31 +229,34 @@ Schema Requests Current reality: -- SignifyPy does not yet expose a dedicated ``client.schemas()`` accessor or a - ``signify.app.schemas`` module. - Schema support is still a maintained workflow because credential issuance and validation depend on resolving schema OOBIs into local state before issuing or admitting credentials. Implemented surface today: +- ``client.schemas().get(said)`` +- ``client.schemas().list()`` - ``client.oobis().resolve(schema_oobi, alias="schema")`` Routes: +- ``GET /schema`` +- ``GET /schema/{said}`` - ``POST /oobis`` Primary tests: +- ``tests/app/test_schemas.py`` - ``tests/integration/test_provisioning_and_identifiers.py`` - ``tests/integration/test_credentials.py`` - ``tests/integration/test_multisig_credentials.py`` Maintainer note: -Document schema support as an OOBI-backed workflow, not as a missing feature. -The real gap is the absence of a dedicated schema resource wrapper, not the -absence of schema behavior in the client. +Document schema support in two layers: dedicated schema reads now exist, and +OOBI-backed schema resolution remains the workflow that makes those reads +useful in real credential flows. Challenge Requests ------------------ @@ -374,7 +396,8 @@ Exchange and IPEX Requests -------------------------- Implementation: -``signify.peer.exchanging.Exchanges`` and +``signify.app.exchanging.Exchanges`` (backed by +``signify.peer.exchanging.Exchanges``) and ``signify.app.credentialing.Ipex`` Routes: @@ -504,7 +527,6 @@ client. It should not be used to imply parity that does not exist. Notable current gaps: -- no dedicated ``schemas()`` resource wrapper yet - no dedicated ``config()`` resource wrapper yet When those surfaces are added, update this guide and the API reference in the diff --git a/docs/signify_app.rst b/docs/signify_app.rst index 2ec195e..1819b00 100644 --- a/docs/signify_app.rst +++ b/docs/signify_app.rst @@ -41,7 +41,13 @@ signify.app.delegating ---------------------- .. automodule:: signify.app.delegating - :members: + :members: + +signify.app.exchanging +---------------------- + +.. automodule:: signify.app.exchanging + :members: signify.app.ending ------------------ @@ -61,6 +67,12 @@ signify.app.grouping .. automodule:: signify.app.grouping :members: +signify.app.schemas +------------------- + +.. automodule:: signify.app.schemas + :members: + signify.app.notifying --------------------- diff --git a/src/signify/app/clienting.py b/src/signify/app/clienting.py index 1fb62b4..0a045d0 100644 --- a/src/signify/app/clienting.py +++ b/src/signify/app/clienting.py @@ -337,9 +337,14 @@ def registries(self): from signify.app.credentialing import Registries return Registries(client=self) + def schemas(self): + """Return the schema read resource wrapper.""" + from signify.app.schemas import Schemas + return Schemas(client=self) + def exchanges(self): - """Return the peer exchange transport resource wrapper.""" - from signify.peer.exchanging import Exchanges + """Return the exchange transport resource wrapper.""" + from signify.app.exchanging import Exchanges return Exchanges(client=self) def ipex(self): diff --git a/src/signify/app/credentialing.py b/src/signify/app/credentialing.py index 30e7146..768135d 100644 --- a/src/signify/app/credentialing.py +++ b/src/signify/app/credentialing.py @@ -9,7 +9,7 @@ """ from collections import namedtuple -from keri.core import coring, counting +from keri.core import coring, counting, serdering from keri.core.eventing import TraitDex, interact from keri.help import helping from keri.vc import proving @@ -22,8 +22,36 @@ CredentialTypes = CredentialTypeage(issued='issued', received='received') +class RegistryResult: + """Write-path wrapper for registry creation results.""" + + def __init__(self, regser, serder, sigs, response): + self.regser = regser + self.serder = serder + self.sigs = sigs + self.response = response + + def op(self): + """Return the decoded operation payload from the stored response.""" + return self.response.json() + + class Registries: - """Resource wrapper for registry lifecycle operations under one identifier.""" + """Resource wrapper for registry lifecycle operations under one identifier. + + The canonical write surface follows the established KERIpy/KERIA/SignifyPy + camelCase style while preserving SignifyTS behavioral compatibility: + + - ``create(name, registryName, ...)`` + - ``createFromEvents(hab, name, registryName, vcp, ixn, sigs)`` + - ``rename(name, registryName, newName)`` + + Compatibility forms remain callable for existing SignifyPy callers: + + - ``create(hab, registryName, ...)`` + - ``create_from_events(hab, registryName, vcp, ixn, sigs)`` + - ``rename(hab, registryName, newName)`` + """ def __init__(self, client: SignifyClient): """Create a registries resource bound to one Signify client. @@ -39,14 +67,61 @@ def get(self, name, registryName): res = self.client.get(f"/identifiers/{name}/registries/{registryName}") return res.json() - def create(self, hab, registryName, noBackers=True, estOnly=False, baks=None, toad=0, nonce=None): + def list(self, name): + """List credential registries under one identifier alias.""" + res = self.client.get(f"/identifiers/{name}/registries") + return res.json() + + def create( + self, + target, + registryName=None, + *, + noBackers=True, + estOnly=None, + baks=None, + toad=0, + nonce=None, + ): """Create and submit a new credential registry inception request. - Returns: - tuple: ``(vcp, anc, sigs, operation)`` for the locally created - registry inception event, its anchoring interaction, its - signatures, and the KERIA operation payload. + Canonical usage follows the ecosystem's established camelCase form: + ``create(name, registryName, *, noBackers=True, baks=None, toad=0, nonce=None)``. + + Compatibility forms stay callable during the parity transition: + ``create(hab, registryName, ...)``. All forms return :class:`RegistryResult`. """ + if isinstance(target, str): + name = target + if registryName is None: + raise TypeError("registryName is required") + hab = self.client.identifiers().get(name) + if estOnly is None: + state_traits = hab["state"].get("c", []) + estOnly = TraitDex.EstOnly in state_traits or "EO" in state_traits + if estOnly: + raise NotImplementedError("establishment only not implemented") + else: + hab = target + if registryName is None: + raise TypeError("registryName is required") + name = hab["name"] + if estOnly is None: + estOnly = False + + return self._create_result( + hab=hab, + name=name, + registryName=registryName, + noBackers=noBackers, + estOnly=estOnly, + baks=baks, + toad=toad, + nonce=nonce, + ) + + def _create_result(self, *, hab, name, registryName, noBackers=True, estOnly=False, baks=None, toad=0, nonce=None): + """Build registry inception events locally and wrap the submission result.""" baks = baks if baks is not None else [] pre = hab["prefix"] @@ -61,12 +136,14 @@ def create(self, hab, registryName, noBackers=True, estOnly=False, baks=None, to if estOnly: cnfg.append(TraitDex.EstOnly) - regser = eventing.incept(pre, - baks=baks, - toad=toad, - nonce=nonce, - cnfg=cnfg, - code=coring.MtrDex.Blake3_256) + regser = eventing.incept( + pre, + baks=baks, + toad=toad, + nonce=nonce, + cnfg=cnfg, + code=coring.MtrDex.Blake3_256, + ) state = hab["state"] sn = int(state["s"], 16) @@ -80,12 +157,27 @@ def create(self, hab, registryName, noBackers=True, estOnly=False, baks=None, to keeper = self.client.manager.get(aid=hab) sigs = keeper.sign(ser=serder.raw) - op = self.create_from_events(hab=hab, registryName=registryName, vcp=regser.ked, ixn=serder.ked, sigs=sigs) - - return regser, serder, sigs, op + response = self._submit_registry_events( + hab=hab, + name=name, + registryName=registryName, + vcp=regser.ked, + ixn=serder.ked, + sigs=sigs, + ) + return RegistryResult(regser=regser, serder=serder, sigs=sigs, response=response) - def create_from_events(self, hab, registryName, vcp, ixn, sigs): - """Submit a registry creation request from prebuilt local events.""" + @staticmethod + def _serder_from_event(event): + """Normalize a registry or anchoring event into a SerderKERI.""" + if hasattr(event, "ked"): + return serdering.SerderKERI(sad=event.ked) + if hasattr(event, "sad"): + return serdering.SerderKERI(sad=event.sad) + return serdering.SerderKERI(sad=event) + + def _submit_registry_events(self, *, hab, name, registryName, vcp, ixn, sigs): + """Submit prebuilt registry inception material and return the raw response.""" body = dict( name=registryName, vcp=vcp, @@ -94,10 +186,38 @@ def create_from_events(self, hab, registryName, vcp, ixn, sigs): ) keeper = self.client.manager.get(aid=hab) body[keeper.algo] = keeper.params() - name = hab["name"] - resp = self.client.post(path=f"/identifiers/{name}/registries", json=body) - return resp.json() + return self.client.post(path=f"/identifiers/{name}/registries", json=body) + + def create_from_events(self, hab, registryName, vcp, ixn, sigs): + """Compatibility wrapper returning the legacy operation JSON payload.""" + return self.createFromEvents( + hab=hab, + name=hab["name"], + registryName=registryName, + vcp=vcp, + ixn=ixn, + sigs=sigs, + ).op() + + def createFromEvents(self, hab, name, registryName, vcp, ixn, sigs): + """Submit a registry creation request from prebuilt local events. + + Returns: + RegistryResult: Wrapper exposing the submitted event material and + the decoded operation payload through ``op()``. + """ + regser = self._serder_from_event(vcp) + serder = self._serder_from_event(ixn) + response = self._submit_registry_events( + hab=hab, + name=name, + registryName=registryName, + vcp=regser.ked, + ixn=serder.ked, + sigs=sigs, + ) + return RegistryResult(regser=regser, serder=serder, sigs=sigs, response=response) @staticmethod def serialize(serder, anc): @@ -121,9 +241,14 @@ def serialize(serder, anc): return msg - def rename(self, hab, registryName, newName): - """Rename a registry alias under an existing identifier.""" - name = hab["name"] + def rename(self, target, registryName, newName): + """Rename a registry alias under an existing identifier. + + Parameters: + target (str | dict): Canonical identifier alias string or legacy + habitat dict carrying ``name``. + """ + name = target if isinstance(target, str) else target["name"] body = dict(name=newName) resp = self.client.put(path=f"/identifiers/{name}/registries/{registryName}", json=body) return resp.json() @@ -368,7 +493,8 @@ def grant(self, hab, recp, message, acdc, iss, anc, agree=None, dt=None): kwa['dig'] = agree.said grant, gsigs, atc = exchanges.createExchangeMessage(sender=hab, route="/ipex/grant", - payload=data, embeds=embeds, recipient=recp, dt=dt, **kwa) + payload=data, embeds=embeds, recipient=recp, + dt=dt, **kwa) return grant, gsigs, atc @@ -407,7 +533,8 @@ def admit(self, hab, message, grant, recp, dt=None): ) admit, asigs, atc = exchanges.createExchangeMessage(sender=hab, route="/ipex/admit", - payload=data, embeds=None, recipient=recp, dt=dt, dig=grant) + payload=data, embeds=None, recipient=recp, + dt=dt, dig=grant) return admit, asigs, atc diff --git a/src/signify/app/exchanging.py b/src/signify/app/exchanging.py new file mode 100644 index 0000000..a9f641a --- /dev/null +++ b/src/signify/app/exchanging.py @@ -0,0 +1,10 @@ +# -*- encoding: utf-8 -*- +"""Canonical app-level exchange helpers for SignifyPy. + +This thin shim keeps the public client surface under ``signify.app`` while the +peer module remains the implementation spine. +""" + +from signify.peer.exchanging import Exchanges + +__all__ = ["Exchanges"] diff --git a/src/signify/app/schemas.py b/src/signify/app/schemas.py new file mode 100644 index 0000000..27b6b65 --- /dev/null +++ b/src/signify/app/schemas.py @@ -0,0 +1,27 @@ +# -*- encoding: utf-8 -*- +"""Schema read helpers for SignifyPy.""" + +from signify.app.clienting import SignifyClient + + +class Schemas: + """Resource wrapper for schema read operations.""" + + def __init__(self, client: SignifyClient): + """Create a schemas resource bound to one Signify client. + + Parameters: + client (SignifyClient): Signify client used to access KERIA schema + endpoints. + """ + self.client = client + + def get(self, said): + """Fetch one schema by SAID.""" + res = self.client.get(f"/schema/{said}") + return res.json() + + def list(self): + """List all schemas currently available to the remote agent.""" + res = self.client.get("/schema") + return res.json() diff --git a/src/signify/peer/exchanging.py b/src/signify/peer/exchanging.py index 4e8cb50..5442d5f 100644 --- a/src/signify/peer/exchanging.py +++ b/src/signify/peer/exchanging.py @@ -58,23 +58,28 @@ def send(self, name, topic, sender, route, payload, embeds, recipients, dig=None return results[0] if len(results) == 1 else results - def createExchangeMessage(self, sender, route, payload, embeds, recipient=None, dig=None, dt=None): - """ Create exn message from parameters and return Serder with signatures and additional attachments. + def createExchangeMessage(self, sender, route, payload, embeds, recipient=None, dig=None, dt=None, datetime=None): + """Create an ``exn`` message plus signatures and attachment material. Parameters: sender (dict): Identifier dict from identifiers.get - route (str): exn route field + route (str): exn route field payload (dict): payload of the exn message embeds (dict): map of label to bytes of encoded KERI event to embed in exn recipient (str): Optional qb64 recipient to mirror TS peer exchange semantics dig (str): Optional qb64 SAID of exchange message reverse chain - dt (str): Iso formatted date string + dt (str): Canonical ISO formatted timestamp for the exn + datetime (str): Compatibility alias for ``dt`` Returns: - (exn, sigs, atc): tuple of Serder, list, bytes of event, signatures over the event and any transposed - attachments from embeds + tuple: ``(exn, sigs, atc)`` for the built exchange message, its + signatures, and attachment material. """ + if dt is not None and datetime is not None and dt != datetime: + raise ValueError("dt and datetime must match when both are provided") + + date = dt if dt is not None else datetime keeper = self.client.manager.get(sender) @@ -84,7 +89,7 @@ def createExchangeMessage(self, sender, route, payload, embeds, recipient=None, recipient=recipient, embeds=embeds, dig=dig, - date=dt) + date=date) sigs = keeper.sign(ser=exn.raw) diff --git a/tests/app/test_challenging.py b/tests/app/test_challenging.py index b56e8ca..a0e4ba3 100644 --- a/tests/app/test_challenging.py +++ b/tests/app/test_challenging.py @@ -105,7 +105,7 @@ def test_challenge_respond(): from signify.app.aiding import Identifiers mock_ids = Identifiers(client=mock_client) # type: ignore - from signify.peer.exchanging import Exchanges + from signify.app.exchanging import Exchanges mock_exc = Exchanges(client=mock_client) # type: ignore from signify.app.challenging import Challenges @@ -139,7 +139,7 @@ def test_challenge_respond_compat_recp_alias(): from signify.app.aiding import Identifiers mock_ids = Identifiers(client=mock_client) # type: ignore - from signify.peer.exchanging import Exchanges + from signify.app.exchanging import Exchanges mock_exc = Exchanges(client=mock_client) # type: ignore from signify.app.challenging import Challenges diff --git a/tests/app/test_clienting.py b/tests/app/test_clienting.py index 7e71880..43d1a86 100644 --- a/tests/app/test_clienting.py +++ b/tests/app/test_clienting.py @@ -701,12 +701,22 @@ def test_signify_client_registries(make_signify_client): assert out.client == client +def test_signify_client_schemas(make_signify_client): + client = make_signify_client() + + out = client.schemas() + + from signify.app.schemas import Schemas + assert type(out) is Schemas + assert out.client == client + + def test_signify_client_exchanges(make_signify_client): client = make_signify_client() out = client.exchanges() - from signify.peer.exchanging import Exchanges + from signify.app.exchanging import Exchanges assert type(out) is Exchanges assert out.client == client diff --git a/tests/app/test_credentialing.py b/tests/app/test_credentialing.py index b01ce4e..1798df4 100644 --- a/tests/app/test_credentialing.py +++ b/tests/app/test_credentialing.py @@ -5,6 +5,7 @@ Testing credentialing with unit tests """ +import pytest from keri.core import eventing, coring from keri.peer import exchanging from keri.vdr import eventing as veventing @@ -13,46 +14,109 @@ from signify.app import credentialing -def test_registries(make_mock_client_with_manager, make_mock_response): +def test_registries_legacy_create_returns_registry_result(make_mock_client_with_manager, make_mock_response): + mock_client, mock_manager = make_mock_client_with_manager() + from signify.core import keeping + mock_response = make_mock_response({}) + mock_hab = {'prefix': 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + 'name': 'aid1', 'state': {'s': '1', 'd': "ABCDEFG"}} + regName = "reg1" + + mock_keeper = mock({'algo': 'salty', 'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True) + expect(mock_manager, times=2).get(aid=mock_hab).thenReturn(mock_keeper) + expect(mock_keeper, times=1).sign(ser=ANY()).thenReturn(['a signature']) + expect(mock_client, times=1).post(path="/identifiers/aid1/registries", json=ANY()).thenReturn(mock_response) + + from signify.app.credentialing import Registries + + registries = Registries(client=mock_client) + result = registries.create(mock_hab, regName) + + assert isinstance(result, credentialing.RegistryResult) + assert result.regser.pre + assert result.serder.pre == mock_hab["prefix"] + assert result.sigs == ['a signature'] + assert result.response == mock_response + + +def test_registries_name_create_returns_registry_result(make_mock_client_with_manager, make_mock_response): mock_client, mock_manager = make_mock_client_with_manager() from signify.app.aiding import Identifiers mock_ids = mock(spec=Identifiers, strict=True) - from signify.core import keeping - mock_response = make_mock_response({'json': lambda: {}}) + + mock_response = make_mock_response({}) mock_hab = {'prefix': 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', 'name': 'aid1', 'state': {'s': '1', 'd': "ABCDEFG"}} - name = "aid1" regName = "reg1" expect(mock_client, times=1).identifiers().thenReturn(mock_ids) - expect(mock_ids, times=1).get(name).thenReturn(mock_hab) - + expect(mock_ids, times=1).get("aid1").thenReturn(mock_hab) mock_keeper = mock({'algo': 'salty', 'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True) expect(mock_manager, times=2).get(aid=mock_hab).thenReturn(mock_keeper) - expect(mock_keeper, times=2).sign(ser=ANY()).thenReturn(['a signature']) - expect(mock_client, times=1).post(path=f"/identifiers/{name}/registries", json=ANY()).thenReturn(mock_response) + expect(mock_keeper, times=1).sign(ser=ANY()).thenReturn(['a signature']) + expect(mock_client, times=1).post(path="/identifiers/aid1/registries", json=ANY()).thenReturn(mock_response) - from signify.app.credentialing import Registries + result = credentialing.Registries(client=mock_client).create( + "aid1", + regName, + nonce="A_NONCE", + ) - registries = Registries(client=mock_client) - registries.create(hab=mock_hab, registryName=regName) + assert isinstance(result, credentialing.RegistryResult) + assert result.regser.pre + assert result.serder.pre == mock_hab["prefix"] + assert result.sigs == ['a signature'] + assert result.response == mock_response + + +def test_registries_name_create_establishment_only_not_implemented(make_mock_client_with_manager): + mock_client, _ = make_mock_client_with_manager() + + from signify.app.aiding import Identifiers + mock_ids = mock(spec=Identifiers, strict=True) + expect(mock_client, times=1).identifiers().thenReturn(mock_ids) + expect(mock_ids, times=1).get("aid1").thenReturn({ + "prefix": "EPREFIX", + "name": "aid1", + "state": {"s": "1", "d": "ABCDEFG", "c": ["EO"]}, + }) + + with pytest.raises(NotImplementedError, match="establishment only not implemented"): + credentialing.Registries(client=mock_client).create("aid1", "reg1") + + +def test_registries_list_get_rename_and_serialize(make_mock_client_with_manager, make_mock_response): + mock_client, _ = make_mock_client_with_manager() + mock_response = make_mock_response({'json': lambda: {}}) + mock_hab = {'prefix': 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + 'name': 'aid1', 'state': {'s': '1', 'd': "ABCDEFG"}} + regName = "reg1" - expect(mock_client, times=1).get(f"/identifiers/{name}/registries/{regName}").thenReturn(mock_response) + registries = credentialing.Registries(client=mock_client) + + expect(mock_client, times=1).get("/identifiers/aid1/registries").thenReturn(mock_response) + registries.list(name="aid1") + + expect(mock_client, times=1).get("/identifiers/aid1/registries/reg1").thenReturn(mock_response) registries.get(name="aid1", registryName=regName) - (expect(mock_client, times=1).put(path=f"/identifiers/{name}/registries/{regName}", json={'name': 'test'}) + (expect(mock_client, times=1).put(path="/identifiers/aid1/registries/reg1", json={'name': 'test'}) .thenReturn(mock_response)) registries.rename(mock_hab, regName, "test") + (expect(mock_client, times=1).put(path="/identifiers/aid1/registries/reg1", json={'name': 'again'}) + .thenReturn(mock_response)) + registries.rename("aid1", regName, "again") + pre = "ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose" dig = "EOgQvKz8ziRn7FdR_ebwK9BkaVOnGeXQOJ87N6hMLrK0" nonce = "ACb_3pGwW3uIjtOg4zRQ66I-SggMcmoyju_uCzuSvgG4" serder = veventing.incept(pre=pre, nonce=nonce) anc = eventing.interact(pre=pre, dig=dig) - msg = Registries.serialize(serder, anc) + msg = credentialing.Registries.serialize(serder, anc) assert msg == (b'{"v":"KERI10JSON00010f_","t":"vcp","d":"EGaypC6sODRFyIuhdFzzFmBU' b'4Xe5SNprALGbltnyHYSz","i":"EGaypC6sODRFyIuhdFzzFmBU4Xe5SNprALGbl' b'tnyHYSz","ii":"ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose","s"' @@ -61,6 +125,81 @@ def test_registries(make_mock_client_with_manager, make_mock_response): b'so7HDiQ9ZS_AfU8BfgGLHEW54H1') +def test_registry_result_op(make_mock_response): + mock_response = make_mock_response({"json": lambda: {}}) + expect(mock_response, times=1).json().thenReturn({"done": True}) + + result = credentialing.RegistryResult( + regser="regser", + serder="serder", + sigs=["a signature"], + response=mock_response, + ) + + assert result.regser == "regser" + assert result.serder == "serder" + assert result.sigs == ["a signature"] + assert result.op() == {"done": True} + + +def test_registries_createFromEvents_returns_registry_result(make_mock_client_with_manager, make_mock_response): + mock_client, mock_manager = make_mock_client_with_manager() + + from signify.core import keeping + mock_hab = {'prefix': 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + 'name': 'aid1', 'state': {'s': '1', 'd': "ABCDEFG"}} + mock_keeper = mock({'algo': 'salty', 'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True) + expect(mock_manager, times=1).get(aid=mock_hab).thenReturn(mock_keeper) + + mock_response = make_mock_response({}) + expect(mock_client, times=1).post(path="/identifiers/aid1/registries", json=ANY()).thenReturn(mock_response) + + vcp = veventing.incept(pre=mock_hab["prefix"], nonce="A_NONCE") + ixn = eventing.interact(pre=mock_hab["prefix"], dig="ABCDEFG") + + result = credentialing.Registries(client=mock_client).createFromEvents( + hab=mock_hab, + name="aid1", + registryName="reg1", + vcp=vcp.ked, + ixn=ixn.ked, + sigs=["a signature"], + ) + + assert isinstance(result, credentialing.RegistryResult) + assert result.regser.said == vcp.said + assert result.serder.said == ixn.said + assert result.sigs == ["a signature"] + assert result.response == mock_response + + +def test_registries_create_from_events_compat_returns_json(make_mock_client_with_manager, make_mock_response): + mock_client, mock_manager = make_mock_client_with_manager() + + from signify.core import keeping + mock_hab = {'prefix': 'ELI7pg979AdhmvrjDeam2eAO2SR5niCgnjAJXJHtJose', + 'name': 'aid1', 'state': {'s': '1', 'd': "ABCDEFG"}} + mock_keeper = mock({'algo': 'salty', 'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True) + expect(mock_manager, times=1).get(aid=mock_hab).thenReturn(mock_keeper) + + mock_response = make_mock_response({}) + expect(mock_client, times=1).post(path="/identifiers/aid1/registries", json=ANY()).thenReturn(mock_response) + expect(mock_response, times=1).json().thenReturn({"done": True}) + + vcp = veventing.incept(pre=mock_hab["prefix"], nonce="A_NONCE") + ixn = eventing.interact(pre=mock_hab["prefix"], dig="ABCDEFG") + + result = credentialing.Registries(client=mock_client).create_from_events( + hab=mock_hab, + registryName="reg1", + vcp=vcp.ked, + ixn=ixn.ked, + sigs=["a signature"], + ) + + assert result == {"done": True} + + def test_registries_create_uses_nb_trait_for_backerless_registry(): """Backerless registry inception must use the VDR `NB` trait code. @@ -106,7 +245,7 @@ def post(self, path, json): } client = DummyClient() - credentialing.Registries(client=client).create(hab=hab, registryName="reg1") + credentialing.Registries(client=client).create(hab, "reg1") assert client.last_post is not None path, body = client.last_post @@ -198,7 +337,7 @@ def test_ipex_grant(): from signify.app.clienting import SignifyClient mock_client = mock(spec=SignifyClient, strict=True) - from signify.peer.exchanging import Exchanges + from signify.app.exchanging import Exchanges mock_excs = mock(spec=Exchanges, strict=True) dt = "2023-09-25T16:01:37.000000+00:00" @@ -233,7 +372,7 @@ def test_ipex_admit(): from signify.app.clienting import SignifyClient mock_client = mock(spec=SignifyClient, strict=True) - from signify.peer.exchanging import Exchanges + from signify.app.exchanging import Exchanges mock_excs = mock(spec=Exchanges, strict=True) grant, _ = exchanging.exchange("/admit/grant", payload={}, sender="EEE") diff --git a/tests/app/test_exchanging.py b/tests/app/test_exchanging.py new file mode 100644 index 0000000..62ff1cd --- /dev/null +++ b/tests/app/test_exchanging.py @@ -0,0 +1,14 @@ +# -*- encoding: utf-8 -*- +""" +SIGNIFY +signify.app.test_exchanging module + +Testing the canonical app exchange import surface. +""" + + +def test_app_exchanges_reexports_peer_exchange_class(): + from signify.app.exchanging import Exchanges + from signify.peer.exchanging import Exchanges as PeerExchanges + + assert Exchanges is PeerExchanges diff --git a/tests/app/test_schemas.py b/tests/app/test_schemas.py new file mode 100644 index 0000000..c3a2d36 --- /dev/null +++ b/tests/app/test_schemas.py @@ -0,0 +1,43 @@ +# -*- encoding: utf-8 -*- +""" +SIGNIFY +signify.app.test_schemas module + +Testing schema read helpers with unit tests. +""" + +from mockito import mock, expect, verifyNoUnwantedInteractions, unstub + + +def test_schemas_get(make_mock_response): + from signify.app.clienting import SignifyClient + mock_client = mock(spec=SignifyClient, strict=True) + + mock_response = make_mock_response({"json": lambda: {}}) + expect(mock_client, times=1).get("/schema/EA_SCHEMA").thenReturn(mock_response) + expect(mock_response, times=1).json().thenReturn({"$id": "EA_SCHEMA"}) + + from signify.app.schemas import Schemas + out = Schemas(client=mock_client).get("EA_SCHEMA") # type: ignore + + assert out == {"$id": "EA_SCHEMA"} + + verifyNoUnwantedInteractions() + unstub() + + +def test_schemas_list(make_mock_response): + from signify.app.clienting import SignifyClient + mock_client = mock(spec=SignifyClient, strict=True) + + mock_response = make_mock_response({"json": lambda: []}) + expect(mock_client, times=1).get("/schema").thenReturn(mock_response) + expect(mock_response, times=1).json().thenReturn([{"$id": "EA_SCHEMA"}]) + + from signify.app.schemas import Schemas + out = Schemas(client=mock_client).list() # type: ignore + + assert out == [{"$id": "EA_SCHEMA"}] + + verifyNoUnwantedInteractions() + unstub() diff --git a/tests/integration/_services/keria_server.py b/tests/integration/_services/keria_server.py index da668a2..dd534b6 100644 --- a/tests/integration/_services/keria_server.py +++ b/tests/integration/_services/keria_server.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +from pathlib import Path import signal @@ -15,8 +16,19 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() +def configure_temp_log_root(config_dir: str) -> None: + """Point KERIA temp logging at this stack's runtime root before import.""" + from hio.help import ogling + + runtime_root = Path(config_dir).resolve().parent + temp_head_dir = runtime_root / "keria-tmp" + temp_head_dir.mkdir(parents=True, exist_ok=True) + ogling.Ogler.TempHeadDir = str(temp_head_dir) + + def main() -> None: args = parse_args() + configure_temp_log_root(args.config_dir) from keria.app import agenting config = agenting.KERIAServerConfig( diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index e54f046..d4f8fec 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -1585,9 +1585,8 @@ def create_registry(client: SignifyClient, issuer_name: str, registry_name: str) registry event, so callers almost always need the refreshed issuer habitat returned here before attempting credential issuance. """ - issuer_hab = client.identifiers().get(issuer_name) - _, _, _, operation = client.registries().create(issuer_hab, registry_name) - wait_for_operation(client, operation) + result = client.registries().create(issuer_name, registry_name) + wait_for_operation(client, result.op()) issuer_hab = client.identifiers().get(issuer_name) registry = client.registries().get(issuer_name, registry_name) return issuer_hab, registry @@ -1595,8 +1594,7 @@ def create_registry(client: SignifyClient, issuer_name: str, registry_name: str) def rename_registry(client: SignifyClient, issuer_name: str, registry_name: str, new_name: str) -> dict: """Rename a registry and return the refreshed renamed registry view.""" - issuer_hab = client.identifiers().get(issuer_name) - client.registries().rename(issuer_hab, registry_name, new_name) + client.registries().rename(issuer_name, registry_name, new_name) return client.registries().get(issuer_name, new_name) @@ -1717,19 +1715,21 @@ def create_multisig_registry( anc = serdering.SerderKERI(sad=proposal["anc"]) keeper = client.manager.get(aid=group_hab) sigs = keeper.sign(ser=anc.raw) - operation = client.registries().create_from_events( + result = client.registries().createFromEvents( hab=group_hab, + name=group_name, registryName=registry_name, vcp=vcp.ked, ixn=anc.ked, sigs=sigs, ) + operation = result.op() else: - vcp, anc, sigs, operation = client.registries().create( - hab=group_hab, - registryName=registry_name, - nonce=nonce, - ) + result = client.registries().create(group_name, registry_name, nonce=nonce) + vcp = result.regser + anc = result.serder + sigs = result.sigs + operation = result.op() client.exchanges().send( local_member_name, "registry", diff --git a/tests/integration/test_provisioning_and_identifiers.py b/tests/integration/test_provisioning_and_identifiers.py index b6f97fc..c549206 100644 --- a/tests/integration/test_provisioning_and_identifiers.py +++ b/tests/integration/test_provisioning_and_identifiers.py @@ -102,9 +102,14 @@ def test_schema_oobi_resolution_smoke(client_factory): client = client_factory() result = resolve_schema_oobi(client) + schema = client.schemas().get(SCHEMA_SAID) + schemas = client.schemas().list() + schema_saids = {entry["$id"] for entry in schemas} assert result["done"] is True assert result["metadata"]["oobi"] == schema_oobi(client) + assert schema["$id"] == SCHEMA_SAID + assert SCHEMA_SAID in schema_saids for alias_name, oobi in additional_schema_oobis(client).items(): extra = resolve_oobi(client, oobi, alias=alias_name) @@ -263,8 +268,8 @@ def test_credential_issue_smoke(client_factory): issuer_hab = create_identifier(client, issuer_name, wits=[]) resolve_schema_oobi(client) - _, _, _, registry_op = client.registries().create(issuer_hab, registry_name) - wait_for_operation(client, registry_op) + registry_result = client.registries().create(issuer_name, registry_name) + wait_for_operation(client, registry_result.op()) # Registry inception anchors itself with an interaction event, so the # identifier state must be reloaded before building the next credential # issuance anchor. diff --git a/tests/peer/test_exchanging.py b/tests/peer/test_exchanging.py index e42ec8e..364dffb 100644 --- a/tests/peer/test_exchanging.py +++ b/tests/peer/test_exchanging.py @@ -62,6 +62,107 @@ def test_exchanges_get(mockHelpingNowIso8601, make_mock_response): assert out == {'content': 'an exn'} + +def test_create_exchange_message_uses_dt(make_mock_client_with_manager, monkeypatch): + mock_client, mock_manager = make_mock_client_with_manager() + + from signify.core import keeping + sender = {'prefix': 'a_prefix', 'name': 'aid1', 'state': {'s': '1', 'd': "ABCDEFG"}} + mock_keeper = mock({'algo': 'salty', 'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True) + expect(mock_manager, times=1).get(sender).thenReturn(mock_keeper) + + captured = {} + + class FakeSerder: + raw = b"raw exn" + + def fake_exchange(*, route, payload, sender, recipient, embeds, dig, date): + captured["route"] = route + captured["payload"] = payload + captured["sender"] = sender + captured["recipient"] = recipient + captured["embeds"] = embeds + captured["dig"] = dig + captured["date"] = date + return FakeSerder(), bytearray(b"") + + monkeypatch.setattr("signify.peer.exchanging.exchanging.exchange", fake_exchange) + expect(mock_keeper, times=1).sign(ser=b"raw exn").thenReturn(['a signature']) + + from signify.peer.exchanging import Exchanges + exn, sigs, atc = Exchanges(client=mock_client).createExchangeMessage( # type: ignore + sender=sender, + route="/ipex/admit", + payload={"a": "b"}, + embeds={}, + recipient="Eqbc123", + dt="2024-01-01T00:00:00+00:00", + dig="EDIG", + ) + + assert exn.raw == b"raw exn" + assert sigs == ['a signature'] + assert atc == "" + assert captured == { + "route": "/ipex/admit", + "payload": {"a": "b"}, + "sender": "a_prefix", + "recipient": "Eqbc123", + "embeds": {}, + "dig": "EDIG", + "date": "2024-01-01T00:00:00+00:00", + } + + +def test_create_exchange_message_accepts_datetime_compat_alias(make_mock_client_with_manager, monkeypatch): + mock_client, mock_manager = make_mock_client_with_manager() + + from signify.core import keeping + sender = {'prefix': 'a_prefix', 'name': 'aid1', 'state': {'s': '1', 'd': "ABCDEFG"}} + mock_keeper = mock({'algo': 'salty', 'params': lambda: {'keeper': 'params'}}, spec=keeping.SaltyKeeper, strict=True) + expect(mock_manager, times=1).get(sender).thenReturn(mock_keeper) + + captured = {} + + class FakeSerder: + raw = b"raw exn" + + def fake_exchange(*, route, payload, sender, recipient, embeds, dig, date): + captured["date"] = date + return FakeSerder(), bytearray(b"") + + monkeypatch.setattr("signify.peer.exchanging.exchanging.exchange", fake_exchange) + expect(mock_keeper, times=1).sign(ser=b"raw exn").thenReturn(['a signature']) + + from signify.peer.exchanging import Exchanges + _, sigs, atc = Exchanges(client=mock_client).createExchangeMessage( # type: ignore + sender=sender, + route="/ipex/admit", + payload={"a": "b"}, + embeds={}, + datetime="2024-01-01T00:00:00+00:00", + ) + + assert sigs == ['a signature'] + assert atc == "" + assert captured["date"] == "2024-01-01T00:00:00+00:00" + + +def test_create_exchange_message_rejects_conflicting_dt_and_datetime(make_mock_client_with_manager): + mock_client, _ = make_mock_client_with_manager() + + from signify.peer.exchanging import Exchanges + + with pytest.raises(ValueError, match="dt and datetime must match"): + Exchanges(client=mock_client).createExchangeMessage( # type: ignore + sender={'prefix': 'a_prefix'}, + route="/ipex/admit", + payload={}, + embeds={}, + dt="2024-01-02T00:00:00+00:00", + datetime="2024-01-01T00:00:00+00:00", + ) + def test_exchanges_send_multiple_recipients(mockHelpingNowIso8601, make_mock_client_with_manager, make_mock_response): mock_client, mock_manager = make_mock_client_with_manager()