"""Signer extension tests — vault, identities, NIP-07, modal overlay.""" import time import pytest from selenium.webdriver.common.by import By class TestExtensionInstall: """Extension installs and injects window.nostr.""" def test_extension_loaded(self, relay, ext): """Extension UUID was extracted successfully.""" assert ext.startswith("moz-extension://") def test_window_nostr(self, relay, browser, ext, h): """After extension install, window.nostr is present on the app page.""" browser.get(relay["url"]) time.sleep(3) assert h.has_nostr(), "window.nostr not found" def test_window_nostr_smesh(self, relay, browser, ext, h): """The smesh management API is also present.""" browser.get(relay["url"]) time.sleep(3) assert h.has_smesh(), "window.nostr.smesh not found" class TestVaultLifecycle: """Create vault → lock → unlock cycle.""" def test_vault_status_none(self, relay, browser, ext, h): """Fresh extension reports vault status 'none'.""" browser.get(relay["url"]) time.sleep(3) status = h.vault_status() assert status == "none", f"expected 'none', got '{status}'" def test_create_vault(self, relay, browser, ext, h): """Create a vault with a test password.""" browser.get(relay["url"]) time.sleep(3) result = h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.createVault('testpass123') .then(function(ok) { cb(ok); }) .catch(function(e) { cb(false); }); """, timeout=15) assert result is True, "createVault returned false" def test_vault_status_unlocked(self, relay, browser, ext, h): """After creation, vault is unlocked.""" browser.get(relay["url"]) time.sleep(3) status = h.vault_status() assert status == "unlocked", f"expected 'unlocked', got '{status}'" def test_lock_vault(self, relay, browser, ext, h): """Lock the vault.""" browser.get(relay["url"]) time.sleep(3) h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.lockVault() .then(function() { cb(true); }) .catch(function() { cb(false); }); """) status = h.vault_status() assert status == "locked", f"expected 'locked', got '{status}'" def test_unlock_vault(self, relay, browser, ext, h): """Unlock with correct password.""" browser.get(relay["url"]) time.sleep(3) result = h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.unlockVault('testpass123') .then(function(ok) { cb(ok); }) .catch(function(e) { cb(false); }); """) assert result is True status = h.vault_status() assert status == "unlocked" def test_unlock_wrong_password(self, relay, browser, ext, h): """Wrong password fails.""" browser.get(relay["url"]) time.sleep(3) # Lock first. h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.lockVault().then(function() { cb(true); }); """) result = h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.unlockVault('wrongpassword') .then(function(ok) { cb(ok); }) .catch(function(e) { cb(false); }); """) assert result is False or result is None # Re-unlock for subsequent tests. h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.unlockVault('testpass123') .then(function(ok) { cb(ok); }); """) class TestIdentities: """Identity management via window.nostr.smesh.""" def test_add_identity(self, relay, browser, ext, h): """Add an nsec identity.""" browser.get(relay["url"]) time.sleep(3) # Ensure vault is unlocked. h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.unlockVault('testpass123') .then(function() { cb(true); }) .catch(function() { cb(true); }); """) # Use a known test nsec (SHA256('smesh-test-key'), bech32-encoded). result = h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.addIdentity('nsec1jz5zyz6np29zg9k7wu5zvnpusuha8ca84v8ahdme9pdwjlalcsjsp59265') .then(function(ok) { cb(ok); }) .catch(function(e) { cb(false); }); """, timeout=10) assert result is True, "addIdentity returned false" def test_list_identities(self, relay, browser, ext, h): """List returns at least one identity.""" browser.get(relay["url"]) time.sleep(3) h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.unlockVault('testpass123') .then(function() { cb(true); }).catch(function() { cb(true); }); """) ids = h.js_async(""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.listIdentities() .then(function(list) { cb(list); }) .catch(function(e) { cb('[]'); }); """) assert ids is not None # ids is a JSON string or parsed array depending on implementation. import json if isinstance(ids, str): ids = json.loads(ids) assert len(ids) > 0, "no identities returned" def test_get_public_key(self, relay, browser, ext, h): """NIP-07 getPublicKey returns the active identity's pubkey.""" browser.get(relay["url"]) time.sleep(3) pk = h.get_public_key() assert pk is not None, "getPublicKey returned null" assert len(pk) == 64, f"pubkey wrong length: {len(pk)}" def test_switch_identity(self, relay, browser, ext, h): """Switch to an identity by pubkey.""" browser.get(relay["url"]) time.sleep(3) pk = h.get_public_key() result = h.js_async(f""" var cb = arguments[arguments.length - 1]; window.nostr.smesh.switchIdentity('{pk}') .then(function(ok) {{ cb(ok); }}) .catch(function(e) {{ cb(false); }}); """) assert result is True class TestSignerModal: """The signer modal overlay in the sm3sh app.""" def test_signer_button_exists(self, relay, browser, ext, h): """The top bar has a signer button (requires being logged in).""" browser.get(relay["url"]) time.sleep(2) # Fake a login by setting a pubkey in localStorage. # Use a dummy 64-char hex pubkey. dummy_pk = "a" * 64 h.js(f"localStorage.setItem('smesh-pubkey', '{dummy_pk}')") browser.refresh() time.sleep(3) page_text = h.js("return document.body.innerText") assert "signer" in page_text.lower(), "signer button text not found" def test_modal_opens(self, relay, browser, ext, h): """Clicking the signer button opens the modal.""" browser.get(relay["url"]) time.sleep(2) h.js("localStorage.setItem('smesh-pubkey', '" + "a" * 64 + "')") browser.refresh() time.sleep(3) # Click the signer button. try: btn = browser.find_element(By.XPATH, "//button[contains(text(),'signer')]") btn.click() time.sleep(1) except Exception: pytest.skip("signer button not found in current view") # Check modal appeared. backdrop = browser.find_elements(By.CSS_SELECTOR, ".signer-backdrop") assert len(backdrop) > 0, "signer modal backdrop not found" def test_modal_closes(self, relay, browser, ext, h): """Clicking the close button dismisses the modal.""" close = browser.find_elements(By.CSS_SELECTOR, ".signer-close") if close: close[0].click() time.sleep(0.5) backdrop = browser.find_elements(By.CSS_SELECTOR, ".signer-backdrop") assert len(backdrop) == 0, "modal still visible after close" class TestNIP07Signing: """NIP-07 event signing flow.""" def test_sign_event(self, relay, browser, ext, h): """Sign a kind-1 event via window.nostr.signEvent.""" browser.get(relay["url"]) time.sleep(3) result = h.js_async(""" var cb = arguments[arguments.length - 1]; var ev = { kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [], content: "test note from selenium" }; window.nostr.signEvent(ev) .then(function(signed) { cb(JSON.stringify(signed)); }) .catch(function(e) { cb('error:' + e.message); }); """, timeout=60) assert result is not None, "signEvent returned null" import json if isinstance(result, str) and result.startswith("error:"): pytest.fail(f"signEvent error: {result}") signed = json.loads(result) if isinstance(result, str) else result assert "sig" in signed, "signed event missing sig field" assert "id" in signed, "signed event missing id field" assert "pubkey" in signed, "signed event missing pubkey field" assert len(signed["sig"]) == 128, f"sig wrong length: {len(signed['sig'])}" assert "id" in signed, "signed event missing id field" assert "pubkey" in signed, "signed event missing pubkey field" assert len(signed["sig"]) == 128, f"sig wrong length: {len(signed['sig'])}"