"""Shared fixtures for smesh Selenium tests. Test regime uses 127.0.0.x loopback addresses: 127.0.0.1:3334 — relay (WebSocket + static files) 127.0.0.2:3334 — reserved (future: second relay for sync tests) All addresses in 127.0.0.0/8 are routable on Linux loopback by default. """ import json import os import signal import subprocess import time import pytest from selenium import webdriver from selenium.webdriver.firefox.options import Options from selenium.webdriver.firefox.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Prefer moxie-built binary; fall back to older names. _bins = ["smesh", "smesh-moxie", "smesh-test"] RELAY_BIN = next((os.path.join(PROJECT_ROOT, b) for b in _bins if os.path.exists(os.path.join(PROJECT_ROOT, b))), "") STATIC_DIR = os.path.join(PROJECT_ROOT, "web", "static") EXT_DIR = os.path.join(PROJECT_ROOT, "web", "ext") XPI_PATH = os.path.join(PROJECT_ROOT, "dist", "smesh-signer.xpi") DATA_DIR = "/tmp/smesh-test-data" RELAY_HOST = "127.0.0.1" RELAY_PORT = 3334 RELAY_URL = f"http://{RELAY_HOST}:{RELAY_PORT}" WS_URL = f"ws://{RELAY_HOST}:{RELAY_PORT}" VAULT_PASSWORD = "testpass123" EXT_ID = "signer@smesh.lol" def _build_xpi(): """Pack the signer extension into an .xpi for Firefox.""" os.makedirs(os.path.dirname(XPI_PATH), exist_ok=True) # Check required files exist. manifest = os.path.join(EXT_DIR, "manifest.json") if not os.path.exists(manifest): pytest.skip(f"Extension not found at {EXT_DIR}") bg_entry = os.path.join(EXT_DIR, "bg", "$entry.mjs") if not os.path.exists(bg_entry): pytest.skip("Extension background not compiled — run: make build-signer-bg") subprocess.run( ["zip", "-r", XPI_PATH, "."], cwd=EXT_DIR, capture_output=True, check=True, ) def _get_ext_uuid(profile_path): """Extract the internal UUID Firefox assigned to the extension.""" import re prefs = os.path.join(profile_path, "prefs.js") for _ in range(15): if os.path.exists(prefs): with open(prefs) as f: for line in f: if "webextensions.uuids" not in line: continue m = re.search(r'"(\{.+\})"', line) if not m: continue raw = m.group(1).replace('\\"', '"') uuids = json.loads(raw) if EXT_ID in uuids: return uuids[EXT_ID] time.sleep(1) return None # ── Fixtures ────────────────────────────────────── @pytest.fixture(scope="session") def relay(): """Start the smesh relay for the test session.""" if not os.path.exists(RELAY_BIN): pytest.skip(f"Relay binary not found — run: make build-relay") os.makedirs(DATA_DIR, exist_ok=True) env = os.environ.copy() env["SMESH_DATA_DIR"] = DATA_DIR env["SMESH_LISTEN"] = f"{RELAY_HOST}:{RELAY_PORT}" env["SMESH_STATIC_DIR"] = STATIC_DIR proc = subprocess.Popen( [RELAY_BIN], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) # Wait for relay to be ready. for _ in range(30): try: import urllib.request urllib.request.urlopen(RELAY_URL, timeout=1) break except Exception: time.sleep(0.2) else: proc.kill() pytest.fail("Relay failed to start within 6 seconds") info = { "proc": proc, "url": RELAY_URL, "ws": WS_URL, "host": RELAY_HOST, "port": RELAY_PORT, } yield info # Log relay stderr if it died. if proc.poll() is not None: stderr = proc.stderr.read().decode(errors="replace") if stderr: print(f"\n=== RELAY STDERR ===\n{stderr}\n=== END ===\n") proc.send_signal(signal.SIGTERM) try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() @pytest.fixture(scope="session") def xpi(): """Build the signer .xpi and return its path.""" _build_xpi() return XPI_PATH @pytest.fixture(scope="session") def browser(request): """Create a Firefox WebDriver instance for the session.""" opts = Options() if not request.config.getoption("--headed", default=False): opts.add_argument("--headless") opts.set_preference("xpinstall.signatures.required", False) opts.set_preference("extensions.autoDisableScopes", 0) opts.set_preference("devtools.console.stdout.content", True) svc = Service(log_output="/dev/null") driver = webdriver.Firefox(options=opts, service=svc) driver.set_script_timeout(30) driver.implicitly_wait(3) yield driver driver.quit() @pytest.fixture(scope="session") def ext(browser, xpi): """Install the signer extension and return its moz-extension:// base URL.""" browser.install_addon(xpi, temporary=True) time.sleep(3) profile = browser.capabilities.get("moz:profile", "") uuid = _get_ext_uuid(profile) if not uuid: pytest.fail("Could not extract extension UUID from Firefox profile") return f"moz-extension://{uuid}" # ── Helpers available to all tests ──────────────── class Helpers: """Utility methods injected into tests via the `h` fixture.""" def __init__(self, driver): self.driver = driver def wait_for(self, locator, timeout=10): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) ) def click(self, locator, timeout=10): el = WebDriverWait(self.driver, timeout).until( EC.element_to_be_clickable(locator) ) el.click() return el def js(self, script, *args): return self.driver.execute_script(script, *args) def js_async(self, script, *args, timeout=10): """Run async JS: script must call arguments[arguments.length-1](result).""" self.driver.set_script_timeout(timeout) return self.driver.execute_async_script(script, *args) def console_logs(self): """Return browser console log entries (requires devtools.console.stdout.content).""" try: return self.driver.get_log("browser") except Exception: return [] def has_nostr(self): return self.js("return typeof window.nostr !== 'undefined'") def has_smesh(self): return self.js("return !!(window.nostr && window.nostr.smesh)") def get_public_key(self): return self.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.getPublicKey() .then(function(k) { cb(k); }) .catch(function(e) { cb(null); }); """) def vault_status(self): return self.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.getVaultStatus() .then(function(s) { cb(s); }) .catch(function(e) { cb(null); }); """) @pytest.fixture def h(browser): return Helpers(browser) # ── CLI options ─────────────────────────────────── def pytest_addoption(parser): parser.addoption( "--headed", action="store_true", default=False, help="Run Firefox in headed (visible) mode", )