"""The installed Windows application, driven the way a person uses it. Opt-in (MESHBAY_WIN_E2E=1): it needs an installed MeshBay, launches it, and starts and stops the real node. Everything it asserts was broken at least once on a real install while every other test passed: - a fresh account signs in and, with nothing else done, has a node set up, started, linked and listed in the sidebar (it used to take `meshbay-node init` in a terminal and a key pasted on the profile page); - Stop leaves no meshbay-node.exe behind and only then says "Stopped"; - Start and Restart answer with a running node, a new one for Restart. Before running it: - install MeshBay with the setup .exe ("background service" is the default; MESHBAY_E2E_MODE=signin|open when another was chosen); - no %LOCALAPPDATA%\\meshbay and no %APPDATA%\\meshbay-client: it refuses to touch an existing node or profile -- move them aside first; - run it from an ordinary terminal. Inside a packaged application (MSIX, such as an IDE or agent installed from the Store) %LOCALAPPDATA% and %APPDATA% are virtualised for every child process: what this test writes there, a node started by Task Scheduler never sees. Start it outside the package (Win32_Process.Create, a plain console window). $env:MESHBAY_WIN_E2E = 1 pytest packages/meshbay-node/tests/test_e2e_windows_app.py -v -s The hub is a throwaway one on loopback (SQLite, a fresh key), never a real one. """ import json import os import secrets import socket import sqlite3 import subprocess import sys import time from pathlib import Path import pytest pytestmark = [ pytest.mark.skipif(sys.platform != "win32", reason="the Windows application"), pytest.mark.skipif(os.environ.get("MESHBAY_WIN_E2E") != "1", reason="opt-in: set MESHBAY_WIN_E2E=1 (see the module docstring)"), pytest.mark.timeout(900), ] # The environment as it is now, at import: the suite's conftest points HOME, # USERPROFILE, LOCALAPPDATA and APPDATA at a throwaway directory for every test, # and an application started under that crashes at once (0x80000003). The # module-scoped fixture launched before it applied; the relaunch in the test # body did not, which looked like a crash of the application. REAL_ENV = dict(os.environ) LOCALAPPDATA = Path(os.environ.get("LOCALAPPDATA", "")) APPDATA = Path(os.environ.get("APPDATA", "")) INSTALL = Path(os.environ.get("MESHBAY_E2E_INSTALL", LOCALAPPDATA / "Programs" / "MeshBay")) NODE_HOME = LOCALAPPDATA / "meshbay" APP_PROFILE = APPDATA / "meshbay-client" MODE = os.environ.get("MESHBAY_E2E_MODE", "service") CDP_PORT = 9333 def _free_port() -> int: with socket.socket() as s: s.bind(("127.0.0.1", 0)) return s.getsockname()[1] def _launch(argv: list[str]) -> None: """Start the application detached, in the real profile.""" log = Path(REAL_ENV.get("TEMP", ".")) / "mb-e2e-app.log" # its console, for a failure subprocess.Popen(argv, creationflags=0x00000008 | 0x00000200, close_fds=True, stdout=log.open("a"), stderr=subprocess.STDOUT, env={**REAL_ENV, "ELECTRON_ENABLE_LOGGING": "1"}) def _node_processes() -> dict[int, int]: """Every meshbay-node.exe, pid -> Windows session number.""" r = subprocess.run(["tasklist", "/FI", "IMAGENAME eq meshbay-node.exe", "/NH", "/FO", "CSV"], capture_output=True, text=True) rows = (line.strip('"').split('","') for line in r.stdout.splitlines()) return {int(c[1]): int(c[3]) for c in rows if len(c) > 3 and c[1].isdigit()} def _node_pids() -> list[int]: return list(_node_processes()) STARTUP_VBS = (APPDATA / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" / "MeshBay Node.vbs") def _wait(what: str, cond, timeout: float, every: float = 1.0): deadline = time.monotonic() + timeout last = None while time.monotonic() < deadline: last = cond() if last: return last time.sleep(every) raise AssertionError(f"timed out after {timeout:.0f}s waiting for {what} (last: {last!r})") SUBMIT = "[...document.querySelectorAll('main form button[type=submit]')].pop().click()" class Page: """The application's window over the DevTools protocol.""" HELPERS = """ var fill = (el, v) => { const proto = el.tagName === 'SELECT' ? HTMLSelectElement.prototype : HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, v); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); }; var clickText = (t) => { const els = [...document.querySelectorAll('button, a')].filter(e => !e.disabled); const el = els.find(e => e.innerText.trim() === t) || els.find(e => e.innerText.trim().startsWith(t)); if (!el) throw new Error('nothing to click named ' + t); el.click(); }; """ def __init__(self): from websockets.sync.client import connect targets = json.loads(_http_get(f"http://127.0.0.1:{CDP_PORT}/json/list")) page = next(t for t in targets if t["type"] == "page" and t["url"].startswith("app://")) self._conn = connect(page["webSocketDebuggerUrl"], max_size=None) self._ws = self._conn.__enter__() self._id = 0 def eval(self, expr: str): self._id += 1 self._ws.send(json.dumps({"id": self._id, "method": "Runtime.evaluate", "params": { "expression": f"{self.HELPERS}\n(async () => ({expr}))()", "awaitPromise": True, "returnByValue": True, "userGesture": True}})) while True: msg = json.loads(self._ws.recv(timeout=120)) if msg.get("id") == self._id: break res = msg["result"] if "exceptionDetails" in res: raise AssertionError(res["exceptionDetails"].get("exception", {}).get("description")) return res["result"].get("value") def text(self) -> str: return self.eval("document.body.innerText") def sidebar(self) -> str: return self.eval("(document.querySelector('aside') || {}).innerText || ''") def click(self, label: str, timeout: float = 30) -> None: """Click a button or link by its label once it is there and enabled.""" time.sleep(0.3) # after a fill, let the page render what it was given _wait(f"a control named {label!r}", lambda: _try(lambda: self.eval(f"(clickText({json.dumps(label)}), 1)")), timeout) def quit(self) -> None: """Quit the application the way its own Quit does (before-quit runs).""" version = json.loads(_http_get(f"http://127.0.0.1:{CDP_PORT}/json/version")) from websockets.sync.client import connect with connect(version["webSocketDebuggerUrl"]) as browser: browser.send(json.dumps({"id": 1, "method": "Browser.close"})) _try(lambda: browser.recv(timeout=5)) def close(self): _try(lambda: self._conn.__exit__(None, None, None)) def _http_get(url: str, token: str | None = None) -> str: import urllib.request req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"} if token else {}) with urllib.request.urlopen(req, timeout=5) as r: return r.read().decode() def _node_status() -> dict | None: import urllib.request try: token = (NODE_HOME / "data" / "ui-token").read_text(encoding="utf-8").strip() with urllib.request.urlopen(f"http://127.0.0.1:18000/api/status?t={token}", timeout=3) as r: return json.loads(r.read()) except (OSError, ValueError): return None @pytest.fixture(scope="module") def hub(tmp_path_factory): pytest.importorskip("meshbay_hub") pytest.importorskip("websockets.sync.client") work = tmp_path_factory.mktemp("hub") port = _free_port() env = {**os.environ, "MESHBAY_DATABASE_URL": f"sqlite+aiosqlite:///{(work / 'hub.db').as_posix()}", "MESHBAY_HUB_ID": "127.0.0.1", "MESHBAY_HUB_KEY": str(work / "hub_private.pem"), "MESHBAY_HUB_HOST": "127.0.0.1", "MESHBAY_HUB_PORT": str(port)} proc = subprocess.Popen([sys.executable, "-m", "uvicorn", "meshbay_hub.app:create_app", "--factory", "--host", "127.0.0.1", "--port", str(port)], env=env, cwd=work, stdout=(work / "hub.log").open("w"), stderr=subprocess.STDOUT) url = f"http://127.0.0.1:{port}" try: _wait("the local hub", lambda: _try(lambda: _http_get(f"{url}/v1/hub/version")), 30) yield {"url": url, "db": work / "hub.db"} finally: proc.kill() def _try(fn): try: return fn() except Exception: return None @pytest.fixture(scope="module") def app(hub): exe = INSTALL / "MeshBay.exe" if not exe.exists(): pytest.skip(f"no installed MeshBay at {INSTALL}") for there in (NODE_HOME / "node.toml", NODE_HOME / "keystore.enc", APP_PROFILE): if there.exists(): pytest.fail(f"{there} exists: this test only runs on a machine with no node " "and no app profile -- move them aside, it never deletes them") task = subprocess.run(["schtasks", "/query", "/tn", "MeshBay Node"], capture_output=True).returncode == 0 if MODE == "service": assert task, "service mode chosen at install, but there is no boot task" else: assert not task, f"{MODE} mode chosen at install, but the boot task is still there" assert STARTUP_VBS.exists() == (MODE == "signin"), "the sign-in launcher" APP_PROFILE.mkdir(parents=True) (APP_PROFILE / "config.json").write_text(json.dumps({"hubBase": hub["url"]}), encoding="utf-8") _launch([str(exe), f"--remote-debugging-port={CDP_PORT}"]) page = _wait("the application window", lambda: _try(Page), 60) try: yield page finally: # What the window showed last: the first thing to read when it failed. last = _try(lambda: page.eval("location.href + ' ' + document.body.innerText")) print(f"\n--- the application's page at the end ---\n{last}") page.close() subprocess.run(["taskkill", "/F", "/IM", "MeshBay.exe"], capture_output=True) subprocess.run([str(INSTALL / "resources" / "node-runtime" / "meshbay-node.exe"), "autostart", "stop"], capture_output=True, timeout=120) def test_a_fresh_account_gets_its_node_set_up_and_can_stop_it(app, hub): page = app username = "e2e" + secrets.token_hex(3) passphrase = "e2e-" + secrets.token_urlsafe(18) # Register. _wait("the sign-in page", lambda: "Register" in page.text(), 30) page.eval("(location.hash = '#/register', 1)") _wait("the registration form", lambda: page.eval( "document.querySelectorAll('main input').length >= 4"), 15) page.eval(f"""(() => {{ const i = document.querySelectorAll('main input'); fill(i[0], {json.dumps(username)}); fill(i[1], {json.dumps(username + '@example.invalid')}); fill(i[2], {json.dumps(passphrase)}); fill(i[3], {json.dumps(passphrase)}); return 1; }})()""") time.sleep(0.5) # the form's state follows the input events a render later page.eval(f"({SUBMIT}, 1)") # Not "recovery key" alone: the registration form says it too. _wait("the recovery key", lambda: "I have saved my recovery key" in page.text(), 90) page.eval("(document.querySelector('input[type=checkbox]').click(), 1)") page.click("Continue") _wait("the verification form", lambda: "Verify" in page.text(), 30) code = sqlite3.connect(hub["db"]).execute( "select code from email_verifications order by rowid desc limit 1").fetchone()[0] page.eval(f"(fill(document.querySelector('main input'), {json.dumps(code)}), 1)") page.click("Verify") _wait("the verified page", lambda: "Go to login" in page.text(), 30) page.click("Go to login") _wait("the login form", lambda: page.eval( "document.querySelectorAll('main input').length >= 2"), 15) page.eval(f"""(() => {{ const i = document.querySelectorAll('main input'); fill(i[0], {json.dumps(username)}); fill(i[1], {json.dumps(passphrase)}); return 1; }})()""") time.sleep(0.5) # the form's state follows the input events a render later page.eval(f"({SUBMIT}, 1)") # Nothing else done: the node is set up, running, linked, and in the sidebar. _wait("the Node section in the sidebar", lambda: "Create group" in page.sidebar(), 120) status = _wait("a running node", lambda: (s := _node_status()) and s.get("status") == "running" and s, 120) toml = (NODE_HOME / "node.toml").read_text(encoding="utf-8") assert f'username = "{username}"' in toml and hub["url"] in toml token = page.eval("JSON.parse(localStorage.getItem('mb_auth')).token") keys = json.loads(_http_get(f"{hub['url']}/v1/users/{username}/pubkeys", token)) assert keys.get("pk_node_ed25519") == status["pk_node_ed25519"] # Where it runs is the mode: session 0 for the boot task's S4U logon, the # signed-in session otherwise. session = _node_processes()[status["pid"]] assert (session == 0) == (MODE == "service"), f"{MODE} mode, node in session {session}" # Stop: no process left, and only then "Stopped". page.eval("(location.hash = '#/node', 1)") _wait("the Node page", lambda: "Restart" in page.text(), 30) page.click("Stop") _wait("no meshbay-node.exe", lambda: not _node_pids(), 60) _wait("the page saying Stopped", lambda: "Stopped" in page.text(), 30) assert _node_status() is None # Start, then Restart: a running node each time, a new one for Restart. page.click("Start") first = _wait("a node after Start", lambda: (s := _node_status()) and s.get("status") == "running" and s, 120) _wait("the page saying Running", lambda: "Running" in page.text(), 30) page.click("Restart") second = _wait("a new node after Restart", lambda: (s := _node_status()) and s.get("status") == "running" and s.get("pid") != first.get("pid") and s, 120) assert len(_node_pids()) == 1, _node_pids() assert second["pk_node_ed25519"] == first["pk_node_ed25519"] if MODE == "open": # Only while MeshBay is open: Quit stops the node, opening starts it. page.quit() _wait("no node after Quit", lambda: not _node_pids(), 60) # A second instance started while the first is still exiting defers # to it (single-instance lock) and quits. _wait("the application gone", lambda: b"MeshBay.exe" not in subprocess.run( ["tasklist", "/FI", "IMAGENAME eq MeshBay.exe", "/NH"], capture_output=True).stdout, 60) _launch([str(INSTALL / "MeshBay.exe"), f"--remote-debugging-port={CDP_PORT}"]) again = _wait("the application window again", lambda: _try(Page), 60) again.close() _wait("a node started with the app", lambda: (s := _node_status()) and s.get("status") == "running", 120) # The CLI that started it is meshbay-node.exe too, for a moment. _wait("a single meshbay-node.exe", lambda: len(_node_pids()) == 1, 20)