139 lines
6.3 KiB
Python
139 lines
6.3 KiB
Python
import uuid
|
|
"""End-to-end test of movim-rs, simulating what the browser does."""
|
|
import asyncio, json, sys, re
|
|
import requests, websockets
|
|
|
|
BASE = "http://127.0.0.1:8099"
|
|
|
|
|
|
class Client:
|
|
def __init__(self, jid, password):
|
|
self.jid, self.password = jid, password
|
|
self.http = requests.Session()
|
|
self.ws = None
|
|
self.received = []
|
|
|
|
def cookie(self):
|
|
return self.http.cookies.get("MOVIM_SESSION_ID")
|
|
|
|
async def connect_ws(self):
|
|
self.ws = await websockets.connect(
|
|
BASE.replace("http", "ws") + "/ws/",
|
|
additional_headers={"Cookie": f"MOVIM_SESSION_ID={self.cookie()}", "Origin": BASE},
|
|
)
|
|
|
|
async def recv_until(self, pred, timeout=15, seen=False):
|
|
if seen:
|
|
for m in self.received:
|
|
if pred(m):
|
|
return m
|
|
end = asyncio.get_event_loop().time() + timeout
|
|
while True:
|
|
left = end - asyncio.get_event_loop().time()
|
|
if left <= 0:
|
|
raise TimeoutError("timeout waiting")
|
|
msg = json.loads(await asyncio.wait_for(self.ws.recv(), left))
|
|
self.received.append(msg)
|
|
if pred(msg):
|
|
return msg
|
|
|
|
async def call(self, widget, func, params=None):
|
|
body = {"w": widget, "f": func}
|
|
if params is not None:
|
|
body["p"] = params
|
|
await self.ws.send(json.dumps({"func": "message", "b": body}))
|
|
|
|
def ajax(self, widget, func, params=None, page="chat", daemon=False):
|
|
body = {"c": page, "w": widget, "f": func}
|
|
if params is not None:
|
|
body["p"] = params
|
|
r = self.http.post(BASE + ("/ajaxd" if daemon else "/ajax"), json={"func": "message", "b": body})
|
|
return r
|
|
|
|
async def login(self):
|
|
r = self.http.get(BASE + "/login")
|
|
assert r.status_code == 200, r.status_code
|
|
await self.connect_ws()
|
|
await self.recv_until(lambda m: m.get("func") == "started")
|
|
await self.ws.send(json.dumps({"func": "register", "host": self.jid.split("@")[1]}))
|
|
await self.recv_until(lambda m: m.get("func") == "registered")
|
|
form = {"username": {"value": self.jid}, "password": {"value": self.password}}
|
|
await self.call("Login", "ajaxLogin", [form, self.cookie(), "Europe/Paris"])
|
|
msg = await self.recv_until(lambda m: m.get("func") in ("MovimUtils.reloadThis", "MovimTpl.fill", "Login.setCookie") and (m.get("func") != "MovimTpl.fill" or m["p"][0] == "#error"), timeout=30)
|
|
if msg["func"] == "Login.setCookie":
|
|
# An existing session is shared
|
|
self.http.cookies.set("MOVIM_SESSION_ID", msg["p"][0], domain="127.0.0.1")
|
|
elif msg["func"] != "MovimUtils.reloadThis":
|
|
raise RuntimeError("login failed: " + re.sub(r"\s+", " ", msg["p"][1])[:300])
|
|
await self.ws.close()
|
|
# Reload the page like the browser would
|
|
r = self.http.get(BASE + "/chat")
|
|
assert r.status_code == 200 and "chat_widget" in r.text, (r.status_code, r.url)
|
|
await self.connect_ws()
|
|
|
|
|
|
async def main():
|
|
alice = Client("alice@localhost", "alicepass")
|
|
bob = Client("bob@localhost", "bobpass")
|
|
await alice.login()
|
|
print("alice logged in")
|
|
await bob.login()
|
|
print("bob logged in")
|
|
# On a fresh database the first login replays the archive (MAM): let it settle
|
|
await asyncio.sleep(3)
|
|
|
|
# Same calls as the chat page on load
|
|
for c in (alice, bob):
|
|
await c.call("Chat", "ajaxInit")
|
|
await c.call("Chats", "ajaxHttpGet")
|
|
await c.call("Rooms", "ajaxHttpGet")
|
|
await c.call("Presence", "ajaxHttpGetPresence")
|
|
await alice.recv_until(lambda m: m.get("func") == "Chat.init")
|
|
print("init ok")
|
|
|
|
# Alice opens a chat with Bob and sends a message
|
|
await alice.call("Chat", "ajaxGet", ["bob@localhost"])
|
|
await alice.recv_until(lambda m: m.get("func") == "Chat.setSpecificElements")
|
|
msgid = str(uuid.uuid4())
|
|
r = alice.ajax("Chat", "ajaxHttpDaemonSendMessage", [msgid, "bob@localhost", "Hello *Bob* 👋 https://movim.eu", False], daemon=True)
|
|
assert r.status_code == 200, r.status_code
|
|
# Match on our message id: older messages may be replayed from the archive
|
|
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and msgid in json.dumps(m))
|
|
print("alice sees her message:", json.dumps(m["p"][0], ensure_ascii=False)[:400])
|
|
|
|
# Bob receives it
|
|
m = await bob.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and msgid in json.dumps(m))
|
|
msg = next(v for p in m["p"][0].values() for v in p.values() if msgid in json.dumps(v))
|
|
print("bob receives:", msg["body"][:200], "| from", msg["jidfrom"])
|
|
await bob.recv_until(lambda m: m.get("func") == "Notif.desktop", seen=True)
|
|
print("bob notified")
|
|
|
|
# Bob opens the discussion and the receipt/displayed flow
|
|
await bob.call("Notif", "ajaxCurrent", ["chat|alice@localhost"])
|
|
await bob.call("Chat", "ajaxGet", ["alice@localhost"])
|
|
await bob.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper")
|
|
await bob.call("Chat", "ajaxDisplayed", ["alice@localhost", msg["id"]])
|
|
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and any(v.get("displayed") for p in m["p"][0].values() for v in p.values()))
|
|
print("alice sees displayed marker")
|
|
|
|
# Bob reacts
|
|
r = bob.ajax("Chat", "ajaxHttpDaemonSendReaction", [msg["mid"], "👍"], daemon=True)
|
|
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and any(v.get("reactionsHtml") for p in m["p"][0].values() for v in p.values()))
|
|
print("alice sees the reaction")
|
|
|
|
# Chat states
|
|
await bob.call("Chat", "ajaxSendComposing", ["alice@localhost", False])
|
|
m = await alice.recv_until(lambda m: m.get("func") == "MovimTpl.fill" and m["p"][0].endswith("-state") and m["p"][1] != "")
|
|
print("alice sees bob composing")
|
|
|
|
# Bob replies, Alice corrects her message
|
|
r = bob.ajax("Chat", "ajaxHttpDaemonSendMessage", [str(uuid.uuid4()), "alice@localhost", "Hi Alice", False, None, msg["mid"]], daemon=True)
|
|
m = await alice.recv_until(lambda m: m.get("func") == "Chat.appendMessagesWrapper" and any(v.get("parent") for p in m["p"][0].values() for v in p.values()))
|
|
print("alice receives the reply with its parent")
|
|
|
|
print("ALL OK")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|