This commit is contained in:
nnduc
2026-10-24 14:03:52 +07:00
commit 4fe046de58
4790 changed files with 311146 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
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())