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())