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 endpoint in witness to query tel event of an aid #887

Merged
merged 2 commits into from
Jan 8, 2025
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
103 changes: 103 additions & 0 deletions src/keri/app/indirecting.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ def setupWitness(hby, alias="witness", mbx=None, aids=None, tcpPort=5631, httpPo
app.add_route("/", httpEnd)
receiptEnd = ReceiptEnd(hab=hab, inbound=cues, aids=aids)
app.add_route("/receipts", receiptEnd)
queryEnd = QueryEnd(hab=hab)
app.add_route("/query", queryEnd)

server = createHttpServer(host, httpPort, app, keypath, certpath, cafilepath)
if not server.reopen():
Expand Down Expand Up @@ -1183,3 +1185,104 @@ def interceptDo(self, tymth=None, tock=0.0):
yield self.tock

yield self.tock


class QueryEnd:
""" Endpoint class for quering witness for KELs and TELs using HTTP GET

"""

def __init__(self, hab):
self.hab = hab
self.reger = viring.Reger(name=hab.name, db=hab.db, temp=False)

def on_get(self, req, rep):
""" Handles GET requests to query KEL or TEL events of a pre from a witness.

Parameters:
req (Request) Falcon HTTP request
rep (Response) Falcon HTTP response

Query Parameters:
typ (string): The type of event data to query for. Accepted values are:
- 'kel': Retrieve KEL events for a specified 'pre'.
- 'tel': Retrieve TEL events based on 'reg' or 'vcid'.
pre (string, optional): For 'kel' queries, the specific 'pre' to query.
sn (int, optional): For "kel" queries. If provided, returns events with seq-num
greater than or equal to `sn`.
reg (string, optional): For 'tel' queries, registry pre. required if `vcid` is not provided.
vcid (string, optional): For 'tel' queries, credential said. required if `reg` is not provided.

Response:
- 200 OK: Returns event data in "application/json+cesr" format.
- 400 Bad Request: Returned if required query parameters are missing or if an invalid `typ` is specified.

Example:
- /query?typ=kel&pre=ELZ1KBCFOmdj1RPu6kMUnzgMBTl4YsHfpw7wIGvLgW5W
- /query?typ=kel&pre=ELZ1KBCFOmdj1RPu6kMUnzgMBTl4YsHfpw7wIGvLgW5W&sn=5
- /query?typ=tel&reg=EHrbPfpRLU9wpFXTzGY-LIo2FjMiljjEnt238eWHb7yZ&vcid=EO5y0jMXS5XKTYBKjCUPmNKPr1FWcWhtKwB2Go2ozvr0

"""

typ = req.get_param("typ")

if not typ:
raise falcon.HTTPBadRequest(description="'typ' query param is required")

if typ == "kel":
pre = req.get_param("pre")

if not pre:
raise falcon.HTTPBadRequest(description="'pre' query param is required")

evnts = bytearray()

sn = req.get_param_as_int("sn")
if sn is not None: ## query for event with seq-num >= sn
preb = pre.encode("utf-8")
dig = self.hab.db.getKeLast(key=dbing.snKey(pre=preb,
sn=sn))
if dig is None:
raise falcon.HTTPBadRequest(description=f"non-existant event at seq-num {sn}")

for dig in self.hab.db.getKelIter(pre, sn=sn):
try:
msg = self.hab.db.cloneEvtMsg(pre=pre, fn=0, dig=dig)
except Exception:
continue # skip this event
evnts.extend(msg)
else:
for msg in self.hab.db.clonePreIter(pre=pre):
evnts.extend(msg)


rep.set_header('Content-Type', "application/json+cesr")
rep.status = falcon.HTTP_200
rep.data = bytes(evnts)

elif typ == "tel":
regk = req.get_param("reg")
vcid = req.get_param("vcid")

if not regk and not vcid:
raise falcon.HTTPBadRequest(description="Either 'reg' or 'vcid' query param is required for TEL query")

evnts = bytearray()
if regk is not None:
cloner = self.reger.clonePreIter(pre=regk)
for msg in cloner:
evnts.extend(msg)

if vcid is not None:
cloner = self.reger.clonePreIter(pre=vcid)
for msg in cloner:
evnts.extend(msg)

rep.set_header('Content-Type', "application/json+cesr")
rep.status = falcon.HTTP_200
rep.data = bytes(evnts)

else:
rep.set_header('Content-Type', "application/json")
rep.text = "unkown query type."
rep.status = falcon.HTTP_400
124 changes: 119 additions & 5 deletions tests/app/test_indirecting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,20 @@

"""
import json
import time

import falcon
from falcon import testing
import hio
import pytest
from hio.core import tcp, http

from hio.core import http
from hio.base import doing, tyming
from hio.help import decking

from keri.app import indirecting, storing, habbing
from keri.core import coring, serdering
from keri import kering
from keri import core
from keri.app import indirecting, storing, habbing, agenting


def test_mailbox_iter():
Expand Down Expand Up @@ -104,9 +109,9 @@ def test_qrymailbox_iter():
with habbing.openHab(name="test", transferable=True, temp=True, salt=b'0123456789abcdef') as (hby, hab):
assert hab.pre == 'EIaGMMWJFPmtXznY1IIiKDIrg-vIyge6mBl2QV8dDjI3'
icp = hab.makeOwnInception()
icpSrdr = serdering.SerderKERI(raw=icp)
icpSrdr = core.serdering.SerderKERI(raw=icp)
qry = hab.query(pre=hab.pre, src=hab.pre, route="/mbx")
srdr = serdering.SerderKERI(raw=qry)
srdr = core.serdering.SerderKERI(raw=qry)

cues = decking.Deck()
mbx = storing.Mailboxer(temp=True)
Expand Down Expand Up @@ -152,6 +157,114 @@ def test_qrymailbox_iter():
next(mbi)


def test_wit_query_ends(seeder):
with habbing.openHby(name="wes", salt=core.Salter(raw=b'wess-the-witness').qb64) as wesHby, \
habbing.openHby(name="pal", salt=core.Salter(raw=b'0123456789abcdef').qb64) as palHby:

wesDoers = indirecting.setupWitness(alias="wes", hby=wesHby, tcpPort=5634, httpPort=5644)
witDoer = agenting.Receiptor(hby=palHby)

wesHab = wesHby.habByName(name="wes")
seeder.seedWitEnds(palHby.db, witHabs=[wesHab], protocols=[kering.Schemes.http])

app = falcon.App()
query_endpoint = indirecting.QueryEnd(wesHab)
app.add_route("/query", query_endpoint)

wesClient = testing.TestClient(app)

opts = dict(
wesHab=wesHab,
palHby=palHby,
witDoer=witDoer,
wesClient=wesClient
)

doers = wesDoers + [witDoer, doing.doify(wit_querier_test_do, **opts)]

limit = 1.0
tock = 0.03125
doist = doing.Doist(tock=tock, limit=limit, doers=doers)
doist.enter()

tymer = tyming.Tymer(tymth=doist.tymen(), duration=doist.limit)

while not tymer.expired:
doist.recur()
time.sleep(doist.tock)
# doist.do(doers=doers)

assert doist.limit == limit

doist.exit()


def wit_querier_test_do(tymth=None, tock=0.0, **opts):
yield tock # enter context

wesHab = opts["wesHab"]
palHby = opts["palHby"]
witDoer = opts["witDoer"]
wesClient = opts["wesClient"]

palHab = palHby.makeHab(name="pal", wits=[wesHab.pre], transferable=True)

assert palHab.pre == "EEWz3RVIvbGWw4VJC7JEZnGCLPYx4-QgWOwAzGnw-g8y"

witDoer.msgs.append(dict(pre=palHab.pre))
while not witDoer.cues:
yield tock

witDoer.cues.popleft()
msg = next(wesHab.db.clonePreIter(pre=palHab.pre))

# Test valid KEL query with 'pre'
res = wesClient.simulate_get("/query", params={"typ": "kel", "pre": palHab.pre})
assert res.status_code == 200
assert res.headers['Content-Type'] == "application/json+cesr"
assert bytearray(res.content) == bytearray(msg)

# Test KEL query without 'pre'
res = wesClient.simulate_get("/query", params={"typ": "kel"})
assert res.status_code == 400
assert res.headers['Content-Type'] == "application/json"
assert "'pre' query param is required" in res.text

# Test KEL query with 'sn' parameter
res = wesClient.simulate_get("/query", params={"typ": "kel", "pre": palHab.pre, "sn": 0})
assert res.status_code == 200
assert res.headers['Content-Type'] == "application/json+cesr"

# Test KEL query with non-existant 'sn' parameter
res = wesClient.simulate_get("/query", params={"typ": "kel", "pre": palHab.pre, "sn": 5})
assert res.status_code == 400
assert res.headers['Content-Type'] == "application/json"
assert "non-existant event at seq-num 5" in res.text

# Test valid TEL query with 'reg'
res = wesClient.simulate_get("/query", params={"typ": "tel", "reg": "mock_reg"})
assert res.status_code == 200
assert res.headers['Content-Type'] == "application/json+cesr"

# Test valid TEL query with 'vcid'
res = wesClient.simulate_get("/query", params={"typ": "tel", "vcid": "mock_vcid"})
assert res.status_code == 200
assert res.headers['Content-Type'] == "application/json+cesr"

# Test TEL query missing both 'reg' and 'vcid'
res = wesClient.simulate_get("/query", params={"typ": "tel"})
assert res.status_code == 400
assert res.headers['Content-Type'] == "application/json"
assert "Either 'reg' or 'vcid' query param is required for TEL query" in res.text

# Test invalid 'typ' parameter
res = wesClient.simulate_get("/query", params={"typ": "invalid"})
assert res.status_code == 400
assert res.headers['Content-Type'] == "application/json"
assert "unkown query type" in res.text



class MockServerTls:
def __init__(self, certify, keypath, certpath, cafilepath, port):
pass
Expand Down Expand Up @@ -183,3 +296,4 @@ def test_createHttpServer(monkeypatch):
if __name__ == "__main__":
test_mailbox_iter()
test_qrymailbox_iter()
test_wit_query_ends()
Loading