From da8e53fe92b9027ec5b547c00d4f54f278fbb1b2 Mon Sep 17 00:00:00 2001 From: Fergal Date: Sat, 9 Mar 2024 15:27:01 +0000 Subject: [PATCH] feat: IPEX apply, offer, agree endpoints (#198) --- src/keria/app/ipexing.py | 224 +++++++++++++++++++++++++-- tests/app/test_ipexing.py | 317 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 532 insertions(+), 9 deletions(-) diff --git a/src/keria/app/ipexing.py b/src/keria/app/ipexing.py index 39ec50bd..9c26222a 100644 --- a/src/keria/app/ipexing.py +++ b/src/keria/app/ipexing.py @@ -20,13 +20,19 @@ def loadEnds(app): app.add_route("/identifiers/{name}/ipex/admit", admitColEnd) grantColEnd = IpexGrantCollectionEnd() app.add_route("/identifiers/{name}/ipex/grant", grantColEnd) + applyColEnd = IpexApplyCollectionEnd() + app.add_route("/identifiers/{name}/ipex/apply", applyColEnd) + offerColEnd = IpexOfferCollectionEnd() + app.add_route("/identifiers/{name}/ipex/offer", offerColEnd) + agreeColEnd = IpexAgreeCollectionEnd() + app.add_route("/identifiers/{name}/ipex/agree", agreeColEnd) class IpexAdmitCollectionEnd: @staticmethod def on_post(req, rep, name): - """ Registries GET endpoint + """ IPEX Admit POST endpoint Parameters: req: falcon.Request HTTP request @@ -34,13 +40,13 @@ def on_post(req, rep, name): name (str): human readable name for AID --- - summary: List credential issuance and revocation registies - description: List credential issuance and revocation registies + summary: Accept a credential being issued or presented in response to an IPEX grant + description: Accept a credential being issued or presented in response to an IPEX grant tags: - Registries responses: 200: - description: array of current credential issuance and revocation registies + description: long running operation of IPEX admit """ agent = req.context.agent @@ -154,7 +160,7 @@ class IpexGrantCollectionEnd: @staticmethod def on_post(req, rep, name): - """ Registries GET endpoint + """ IPEX Grant POST endpoint Parameters: req: falcon.Request HTTP request @@ -162,13 +168,13 @@ def on_post(req, rep, name): name (str): human readable name for AID --- - summary: List credential issuance and revocation registies - description: List credential issuance and revocation registies + summary: Reply to IPEX agree message or initiate an IPEX exchange with a credential issuance or presentation + description: Reply to IPEX agree message or initiate an IPEX exchange with a credential issuance or presentation tags: - - Registries + - Credentials responses: 200: - description: array of current credential issuance and revocation registies + description: long running operation of IPEX grant """ agent = req.context.agent @@ -266,3 +272,203 @@ def sendMultisigExn(agent, hab, ked, sigs, atc, rec): agent.grants.append(dict(said=grant['d'], pre=hab.pre, rec=[holder])) return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) + + +class IpexApplyCollectionEnd: + + @staticmethod + def on_post(req, rep, name): + """ IPEX Apply POST endpoint + + Parameters: + req: falcon.Request HTTP request + rep: falcon.Response HTTP response + name (str): human readable name for AID + + --- + summary: Request a credential from another party by initiating an IPEX exchange + description: Request a credential from another party by initiating an IPEX exchange + tags: + - Credentials + responses: + 200: + description: long running operation of IPEX apply + + """ + agent = req.context.agent + # Get the hab + hab = agent.hby.habByName(name) + if hab is None: + raise falcon.HTTPNotFound(description=f"alias={name} is not a valid reference to an identifier") + + body = req.get_media() + + ked = httping.getRequiredParam(body, "exn") + sigs = httping.getRequiredParam(body, "sigs") + rec = httping.getRequiredParam(body, "rec") + + route = ked['r'] + + match route: + case "/ipex/apply": + op = IpexApplyCollectionEnd.sendApply(agent, hab, ked, sigs, rec) + case _: + raise falcon.HTTPBadRequest(description=f"invalid message route {route}") + + rep.status = falcon.HTTP_200 + rep.data = op.to_json().encode("utf-8") + + @staticmethod + def sendApply(agent, hab, ked, sigs, rec): + for recp in rec: # Have to verify we already know all the recipients. + if recp not in agent.hby.kevers: + raise falcon.HTTPBadRequest(description=f"attempt to send to unknown AID={recp}") + + # use that data to create th Serder and Sigers for the exn + serder = serdering.SerderKERI(sad=ked) + sigers = [coring.Siger(qb64=sig) for sig in sigs] + + # Now create the stream to send, need the signer seal + kever = hab.kever + seal = eventing.SealEvent(i=hab.pre, s="{:x}".format(kever.lastEst.s), d=kever.lastEst.d) + + # in this case, ims is a message is a sealed and signed message - signed by Signify (KERIA can't sign anything here...) + ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) + + # make a copy and parse + agent.hby.psr.parseOne(ims=bytearray(ims)) + + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) + return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) + +class IpexOfferCollectionEnd: + + @staticmethod + def on_post(req, rep, name): + """ IPEX Offer POST endpoint + + Parameters: + req: falcon.Request HTTP request + rep: falcon.Response HTTP response + name (str): human readable name for AID + + --- + summary: Reply to IPEX apply message or initiate an IPEX exchange with an offer for a credential with certain characteristics + description: Reply to IPEX apply message or initiate an IPEX exchange with an offer for a credential with certain characteristics + tags: + - Credentials + responses: + 200: + description: long running operation of IPEX offer + + """ + agent = req.context.agent + hab = agent.hby.habByName(name) + if hab is None: + raise falcon.HTTPNotFound(description=f"alias={name} is not a valid reference to an identifier") + + body = req.get_media() + + ked = httping.getRequiredParam(body, "exn") + sigs = httping.getRequiredParam(body, "sigs") + atc = httping.getRequiredParam(body, "atc") + rec = httping.getRequiredParam(body, "rec") + + route = ked['r'] + + match route: + case "/ipex/offer": + op = IpexOfferCollectionEnd.sendOffer(agent, hab, ked, sigs, atc, rec) + case _: + raise falcon.HTTPBadRequest(description=f"invalid route {route}") + + rep.status = falcon.HTTP_200 + rep.data = op.to_json().encode("utf-8") + + @staticmethod + def sendOffer(agent, hab, ked, sigs, atc, rec): + for recp in rec: # Have to verify we already know all the recipients. + if recp not in agent.hby.kevers: + raise falcon.HTTPBadRequest(description=f"attempt to send to unknown AID={recp}") + + # use that data to create th Serder and Sigers for the exn + serder = serdering.SerderKERI(sad=ked) + sigers = [coring.Siger(qb64=sig) for sig in sigs] + + # Now create the stream to send, need the signer seal + kever = hab.kever + seal = eventing.SealEvent(i=hab.pre, s="{:x}".format(kever.lastEst.s), d=kever.lastEst.d) + + ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) + ims = ims + atc.encode("utf-8") + + # make a copy and parse + agent.hby.psr.parseOne(ims=bytearray(ims)) + + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) + return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) + +class IpexAgreeCollectionEnd: + + @staticmethod + def on_post(req, rep, name): + """ IPEX Agree POST endpoint + + Parameters: + req: falcon.Request HTTP request + rep: falcon.Response HTTP response + name (str): human readable name for AID + + --- + summary: Reply to IPEX offer message acknowledged willingness to accept offered credential + description: Reply to IPEX offer message acknowledged willingness to accept offered credential + tags: + - Credentials + responses: + 200: + description: long running operation of IPEX agree + + """ + agent = req.context.agent + hab = agent.hby.habByName(name) + if hab is None: + raise falcon.HTTPNotFound(description=f"alias={name} is not a valid reference to an identifier") + + body = req.get_media() + + ked = httping.getRequiredParam(body, "exn") + sigs = httping.getRequiredParam(body, "sigs") + rec = httping.getRequiredParam(body, "rec") + + route = ked['r'] + + match route: + case "/ipex/agree": + op = IpexAgreeCollectionEnd.sendAgree(agent, hab, ked, sigs, rec) + case _: + raise falcon.HTTPBadRequest(description=f"invalid route {route}") + + rep.status = falcon.HTTP_200 + rep.data = op.to_json().encode("utf-8") + + @staticmethod + def sendAgree(agent, hab, ked, sigs, rec): + for recp in rec: # Have to verify we already know all the recipients. + if recp not in agent.hby.kevers: + raise falcon.HTTPBadRequest(description=f"attempt to send to unknown AID={recp}") + + # use that data to create th Serder and Sigers for the exn + serder = serdering.SerderKERI(sad=ked) + sigers = [coring.Siger(qb64=sig) for sig in sigs] + + # Now create the stream to send, need the signer seal + kever = hab.kever + seal = eventing.SealEvent(i=hab.pre, s="{:x}".format(kever.lastEst.s), d=kever.lastEst.d) + + ims = eventing.messagize(serder=serder, sigers=sigers, seal=seal) + + # make a copy and parse + agent.hby.psr.parseOne(ims=bytearray(ims)) + + agent.exchanges.append(dict(said=serder.said, pre=hab.pre, rec=rec, topic='credential')) + return agent.monitor.submit(serder.pre, longrunning.OpTypes.exchange, metadata=dict(said=serder.said)) diff --git a/tests/app/test_ipexing.py b/tests/app/test_ipexing.py index c2c42a65..06bfd9e4 100644 --- a/tests/app/test_ipexing.py +++ b/tests/app/test_ipexing.py @@ -36,6 +36,12 @@ def test_load_ends(helpers): assert isinstance(end, ipexing.IpexAdmitCollectionEnd) (end, *_) = app._router.find("/identifiers/NAME/ipex/grant") assert isinstance(end, ipexing.IpexGrantCollectionEnd) + (end, *_) = app._router.find("/identifiers/NAME/ipex/apply") + assert isinstance(end, ipexing.IpexApplyCollectionEnd) + (end, *_) = app._router.find("/identifiers/NAME/ipex/offer") + assert isinstance(end, ipexing.IpexOfferCollectionEnd) + (end, *_) = app._router.find("/identifiers/NAME/ipex/agree") + assert isinstance(end, ipexing.IpexAgreeCollectionEnd) def test_ipex_admit(helpers, mockHelpingNowIso8601): @@ -919,3 +925,314 @@ def test_granter(helpers): doist.recur(deeds=deeds) assert len(grants) == 1 + + +def test_ipex_apply(helpers, mockHelpingNowIso8601): + with helpers.openKeria() as (_, agent, app, client): + applyEnd = ipexing.IpexApplyCollectionEnd() + app.add_route("/identifiers/{name}/ipex/apply", applyEnd) + + end = aiding.IdentifierCollectionEnd() + app.add_route("/identifiers", end) + aidEnd = aiding.IdentifierResourceEnd() + app.add_route("/identifiers/{name}", aidEnd) + + salt = b'0123456789abcdef' + op = helpers.createAid(client, "test", salt) + aid = op["response"] + pre = aid['i'] + assert pre == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" + + salt2 = b'0123456789abcdeg' + op = helpers.createAid(client, "recp", salt2) + aid1 = op["response"] + pre1 = aid1['i'] + assert pre1 == "EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm" + + applySerder, end = exchanging.exchange(route="/ipex/apply", + payload={'m': 'Applying for a credential', 's': 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', 'a': {'LEI': '78I9GKEFM361IFY3PIN0'}}, + sender=pre, + embeds=dict(), + recipient=pre1, + date=helping.nowIso8601()) + assert applySerder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'Applying for a credential', 's': 'EBfdlu8R27Fbx-ehrqwImnK-8Cm79sqbAQ4MmvEAYqao', 'a': {'LEI': '78I9GKEFM361IFY3PIN0'}}, + 'd': 'EJq6zSDUWw6iaBz8n1cY5cAW3Rrgp4E3sUsoz5JkoMZc', + 'dt': '2021-06-27T21:26:21.233257+00:00', + 'e': {}, + 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'p': '', + 'q': {}, + 'r': '/ipex/apply', + 't': 'exn', + 'v': 'KERI10JSON000187_'} + assert end == b'' + sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] + + body = dict( + exn=applySerder.ked, + sigs=sigs, + atc="", + rec=["EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"] + ) + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/apply", body=data) + + assert res.status_code == 400 + assert res.json == {'description': 'attempt to send to unknown ' + 'AID=EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM', + 'title': '400 Bad Request'} + + body = dict( + exn=applySerder.ked, + sigs=sigs, + atc="", + rec=[pre1] + ) + + # Bad Sender + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/BAD/ipex/apply", body=data) + assert res.status_code == 404 + assert res.json == {'description': 'alias=BAD is not a valid reference to an identifier', + 'title': '404 Not Found'} + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/apply", body=data) + assert res.json == {'done': False, + 'error': None, + 'metadata': {'said': 'EJq6zSDUWw6iaBz8n1cY5cAW3Rrgp4E3sUsoz5JkoMZc'}, + 'name': 'exchange.EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'response': None} + + assert res.status_code == 200 + assert len(agent.exchanges) == 1 + + +def test_ipex_offer(helpers, mockHelpingNowIso8601): + with helpers.openKeria() as (_, agent, app, client): + offerEnd = ipexing.IpexOfferCollectionEnd() + app.add_route("/identifiers/{name}/ipex/offer", offerEnd) + + end0 = aiding.IdentifierCollectionEnd() + app.add_route("/identifiers", end0) + aidEnd = aiding.IdentifierResourceEnd() + app.add_route("/identifiers/{name}", aidEnd) + + salt = b'0123456789abcdef' + op = helpers.createAid(client, "test", salt) + aid = op["response"] + pre = aid['i'] + assert pre == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" + + salt2 = b'0123456789abcdeg' + op = helpers.createAid(client, "recp", salt2) + aid1 = op["response"] + pre1 = aid1['i'] + assert pre1 == "EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm" + + # This should be a metadata ACDC in reality + acdc = b'{"v":"ACDC10JSON000197_","d":"EBg1YzKmwZIDzZsMslTFwQARB6nUN85sRJF5oywlJr3N","i":"EIqTaQiZw73plMOq8pqHTi9BDgDrrE7iE9v2XfN2Izze","ri":"EACehJRd0wfteUAJgaTTJjMSaQqWvzeeHqAMMqxuqxU4","s":"EFgnk_c08WmZGgv9_mpldibRuqFMTQN-rAgtD-TCOwbs","a":{"d":"ELJ7Emhi0Bhxz3s7HyhZ45qcsgpvsT8p8pxwWkG362n3","dt":"2021-06-27T21:26:21.233257+00:00","i":"EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm","LEI":"78I9GKEFM361IFY3PIN0"}}' + embeds = dict(acdc = acdc) + + # First an offer initiated by discloser (no apply) + offer0Serder, end0 = exchanging.exchange(route="/ipex/offer", + payload={'m': 'Offering this'}, + sender=pre, + embeds=embeds, + recipient=pre1, + date=helping.nowIso8601()) + assert offer0Serder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'Offering this'}, + 'd': 'EDY-IFIMBR4umlYATxAqEAcT5jiHEMn5EyL6i1sUwxDO', + 'dt': '2021-06-27T21:26:21.233257+00:00', + 'e': {'acdc': {'a': {'d': 'ELJ7Emhi0Bhxz3s7HyhZ45qcsgpvsT8p8pxwWkG362n3', + 'dt': '2021-06-27T21:26:21.233257+00:00', + 'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', + 'LEI': '78I9GKEFM361IFY3PIN0'}, + 'd': 'EBg1YzKmwZIDzZsMslTFwQARB6nUN85sRJF5oywlJr3N', + 'i': 'EIqTaQiZw73plMOq8pqHTi9BDgDrrE7iE9v2XfN2Izze', + 'ri': 'EACehJRd0wfteUAJgaTTJjMSaQqWvzeeHqAMMqxuqxU4', + 's': 'EFgnk_c08WmZGgv9_mpldibRuqFMTQN-rAgtD-TCOwbs', + 'v': 'ACDC10JSON000197_'}, + 'd': 'EEcYZMP-zilz2w1w2hEFm6tF0eaX_1KaPEWhNfY3kf8i'}, + 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'p': '', + 'q': {}, + 'r': '/ipex/offer', + 't': 'exn', + 'v': 'KERI10JSON0002f6_'} + assert end0 == b'' + sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] + + body = dict( + exn=offer0Serder.ked, + sigs=sigs, + atc="", + rec=["EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"] + ) + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/offer", body=data) + + assert res.status_code == 400 + assert res.json == {'description': 'attempt to send to unknown ' + 'AID=EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM', + 'title': '400 Bad Request'} + + body = dict( + exn=offer0Serder.ked, + sigs=sigs, + atc="", + rec=[pre1] + ) + + # Bad Sender + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/BAD/ipex/offer", body=data) + assert res.status_code == 404 + assert res.json == {'description': 'alias=BAD is not a valid reference to an identifier', + 'title': '404 Not Found'} + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/offer", body=data) + assert res.json == {'done': False, + 'error': None, + 'metadata': {'said': 'EDY-IFIMBR4umlYATxAqEAcT5jiHEMn5EyL6i1sUwxDO'}, + 'name': 'exchange.EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'response': None} + + assert res.status_code == 200 + assert len(agent.exchanges) == 1 + + # Now an offer in response to an agree + dig = "EB_Lr3fHezn1ygn-wbBT5JjzaCMxTmhUoegXeZzWC2eT" + offer1Serder, end1 = exchanging.exchange(route="/ipex/offer", + payload={'m': 'How about this'}, + sender=pre, + embeds=embeds, + dig=dig, + recipient=pre1, + date=helping.nowIso8601()) + assert offer1Serder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'How about this'}, + 'd': 'EDT7go7TfCTzeFnhNBl19JJqdabBfBx8tjBvi_asFCwT', + 'dt': '2021-06-27T21:26:21.233257+00:00', + 'e': {'acdc': {'a': {'d': 'ELJ7Emhi0Bhxz3s7HyhZ45qcsgpvsT8p8pxwWkG362n3', + 'dt': '2021-06-27T21:26:21.233257+00:00', + 'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', + 'LEI': '78I9GKEFM361IFY3PIN0'}, + 'd': 'EBg1YzKmwZIDzZsMslTFwQARB6nUN85sRJF5oywlJr3N', + 'i': 'EIqTaQiZw73plMOq8pqHTi9BDgDrrE7iE9v2XfN2Izze', + 'ri': 'EACehJRd0wfteUAJgaTTJjMSaQqWvzeeHqAMMqxuqxU4', + 's': 'EFgnk_c08WmZGgv9_mpldibRuqFMTQN-rAgtD-TCOwbs', + 'v': 'ACDC10JSON000197_'}, + 'd': 'EEcYZMP-zilz2w1w2hEFm6tF0eaX_1KaPEWhNfY3kf8i'}, + 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'p': 'EB_Lr3fHezn1ygn-wbBT5JjzaCMxTmhUoegXeZzWC2eT', + 'q': {}, + 'r': '/ipex/offer', + 't': 'exn', + 'v': 'KERI10JSON000323_'} + assert end1 == b'' + sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] + + body = dict( + exn=offer1Serder.ked, + sigs=sigs, + atc="", + rec=[pre1] + ) + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/offer", body=data) + assert res.json == {'done': False, + 'error': None, + 'metadata': {'said': 'EDT7go7TfCTzeFnhNBl19JJqdabBfBx8tjBvi_asFCwT'}, + 'name': 'exchange.EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'response': None} + + assert res.status_code == 200 + assert len(agent.exchanges) == 2 + + +def test_ipex_agree(helpers, mockHelpingNowIso8601): + with helpers.openKeria() as (_, agent, app, client): + agreeEnd = ipexing.IpexAgreeCollectionEnd() + app.add_route("/identifiers/{name}/ipex/agree", agreeEnd) + + end = aiding.IdentifierCollectionEnd() + app.add_route("/identifiers", end) + aidEnd = aiding.IdentifierResourceEnd() + app.add_route("/identifiers/{name}", aidEnd) + + salt = b'0123456789abcdef' + op = helpers.createAid(client, "test", salt) + aid = op["response"] + pre = aid['i'] + assert pre == "EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY" + + salt2 = b'0123456789abcdeg' + op = helpers.createAid(client, "recp", salt2) + aid1 = op["response"] + pre1 = aid1['i'] + assert pre1 == "EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm" + dig = "EB_Lr3fHezn1ygn-wbBT5JjzaCMxTmhUoegXeZzWC2eT" + + offerSerder, end = exchanging.exchange(route="/ipex/agree", + payload={'m': 'Agreed'}, + sender=pre, + embeds=dict(), + dig=dig, + recipient=pre1, + date=helping.nowIso8601()) + assert offerSerder.ked == {'a': {'i': 'EFnYGvF_ENKJ_4PGsWsvfd_R6m5cN-3KYsz_0mAuNpCm', 'm': 'Agreed'}, + 'd': 'ECxQe2TgUCRjbbxyCaXMEp6EtSMaqPmDstetoi4bEUrG', + 'dt': '2021-06-27T21:26:21.233257+00:00', + 'e': {}, + 'i': 'EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'p': 'EB_Lr3fHezn1ygn-wbBT5JjzaCMxTmhUoegXeZzWC2eT', + 'q': {}, + 'r': '/ipex/agree', + 't': 'exn', + 'v': 'KERI10JSON00014a_'} + assert end == b'' + sigs = ["AAAa70b4QnTOtGOsMqcezMtVzCFuRJHGeIMkWYHZ5ZxGIXM0XDVAzkYdCeadfPfzlKC6dkfiwuJ0IzLOElaanUgH"] + + body = dict( + exn=offerSerder.ked, + sigs=sigs, + atc="", + rec=["EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM"] + ) + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/agree", body=data) + + assert res.status_code == 400 + assert res.json == {'description': 'attempt to send to unknown ' + 'AID=EZ-i0d8JZAoTNZH3ULaU6JR2nmwyvYAfSVPzhzS6b5CM', + 'title': '400 Bad Request'} + + body = dict( + exn=offerSerder.ked, + sigs=sigs, + atc="", + rec=[pre1] + ) + + # Bad Sender + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/BAD/ipex/agree", body=data) + assert res.status_code == 404 + assert res.json == {'description': 'alias=BAD is not a valid reference to an identifier', + 'title': '404 Not Found'} + + data = json.dumps(body).encode("utf-8") + res = client.simulate_post(path="/identifiers/test/ipex/agree", body=data) + assert res.json == {'done': False, + 'error': None, + 'metadata': {'said': 'ECxQe2TgUCRjbbxyCaXMEp6EtSMaqPmDstetoi4bEUrG'}, + 'name': 'exchange.EHgwVwQT15OJvilVvW57HE4w0-GPs_Stj2OFoAHZSysY', + 'response': None} + + assert res.status_code == 200 + assert len(agent.exchanges) == 1