test_signer.py raw

   1  """Signer extension tests — vault, identities, NIP-07, modal overlay."""
   2  
   3  import time
   4  
   5  import pytest
   6  from selenium.webdriver.common.by import By
   7  
   8  
   9  class TestExtensionInstall:
  10      """Extension installs and injects window.nostr."""
  11  
  12      def test_extension_loaded(self, relay, ext):
  13          """Extension UUID was extracted successfully."""
  14          assert ext.startswith("moz-extension://")
  15  
  16      def test_window_nostr(self, relay, browser, ext, h):
  17          """After extension install, window.nostr is present on the app page."""
  18          browser.get(relay["url"])
  19          time.sleep(3)
  20          assert h.has_nostr(), "window.nostr not found"
  21  
  22      def test_window_nostr_smesh(self, relay, browser, ext, h):
  23          """The smesh management API is also present."""
  24          browser.get(relay["url"])
  25          time.sleep(3)
  26          assert h.has_smesh(), "window.nostr.smesh not found"
  27  
  28  
  29  class TestVaultLifecycle:
  30      """Create vault → lock → unlock cycle."""
  31  
  32      def test_vault_status_none(self, relay, browser, ext, h):
  33          """Fresh extension reports vault status 'none'."""
  34          browser.get(relay["url"])
  35          time.sleep(3)
  36          status = h.vault_status()
  37          assert status == "none", f"expected 'none', got '{status}'"
  38  
  39      def test_create_vault(self, relay, browser, ext, h):
  40          """Create a vault with a test password."""
  41          browser.get(relay["url"])
  42          time.sleep(3)
  43          result = h.js_async("""
  44              var cb = arguments[arguments.length - 1];
  45              window.nostr.smesh.createVault('testpass123')
  46                  .then(function(ok) { cb(ok); })
  47                  .catch(function(e) { cb(false); });
  48          """, timeout=15)
  49          assert result is True, "createVault returned false"
  50  
  51      def test_vault_status_unlocked(self, relay, browser, ext, h):
  52          """After creation, vault is unlocked."""
  53          browser.get(relay["url"])
  54          time.sleep(3)
  55          status = h.vault_status()
  56          assert status == "unlocked", f"expected 'unlocked', got '{status}'"
  57  
  58      def test_lock_vault(self, relay, browser, ext, h):
  59          """Lock the vault."""
  60          browser.get(relay["url"])
  61          time.sleep(3)
  62          h.js_async("""
  63              var cb = arguments[arguments.length - 1];
  64              window.nostr.smesh.lockVault()
  65                  .then(function() { cb(true); })
  66                  .catch(function() { cb(false); });
  67          """)
  68          status = h.vault_status()
  69          assert status == "locked", f"expected 'locked', got '{status}'"
  70  
  71      def test_unlock_vault(self, relay, browser, ext, h):
  72          """Unlock with correct password."""
  73          browser.get(relay["url"])
  74          time.sleep(3)
  75          result = h.js_async("""
  76              var cb = arguments[arguments.length - 1];
  77              window.nostr.smesh.unlockVault('testpass123')
  78                  .then(function(ok) { cb(ok); })
  79                  .catch(function(e) { cb(false); });
  80          """)
  81          assert result is True
  82          status = h.vault_status()
  83          assert status == "unlocked"
  84  
  85      def test_unlock_wrong_password(self, relay, browser, ext, h):
  86          """Wrong password fails."""
  87          browser.get(relay["url"])
  88          time.sleep(3)
  89          # Lock first.
  90          h.js_async("""
  91              var cb = arguments[arguments.length - 1];
  92              window.nostr.smesh.lockVault().then(function() { cb(true); });
  93          """)
  94          result = h.js_async("""
  95              var cb = arguments[arguments.length - 1];
  96              window.nostr.smesh.unlockVault('wrongpassword')
  97                  .then(function(ok) { cb(ok); })
  98                  .catch(function(e) { cb(false); });
  99          """)
 100          assert result is False or result is None
 101          # Re-unlock for subsequent tests.
 102          h.js_async("""
 103              var cb = arguments[arguments.length - 1];
 104              window.nostr.smesh.unlockVault('testpass123')
 105                  .then(function(ok) { cb(ok); });
 106          """)
 107  
 108  
 109  class TestIdentities:
 110      """Identity management via window.nostr.smesh."""
 111  
 112      def test_add_identity(self, relay, browser, ext, h):
 113          """Add an nsec identity."""
 114          browser.get(relay["url"])
 115          time.sleep(3)
 116          # Ensure vault is unlocked.
 117          h.js_async("""
 118              var cb = arguments[arguments.length - 1];
 119              window.nostr.smesh.unlockVault('testpass123')
 120                  .then(function() { cb(true); })
 121                  .catch(function() { cb(true); });
 122          """)
 123          # Use a known test nsec (SHA256('smesh-test-key'), bech32-encoded).
 124          result = h.js_async("""
 125              var cb = arguments[arguments.length - 1];
 126              window.nostr.smesh.addIdentity('nsec1jz5zyz6np29zg9k7wu5zvnpusuha8ca84v8ahdme9pdwjlalcsjsp59265')
 127                  .then(function(ok) { cb(ok); })
 128                  .catch(function(e) { cb(false); });
 129          """, timeout=10)
 130          assert result is True, "addIdentity returned false"
 131  
 132      def test_list_identities(self, relay, browser, ext, h):
 133          """List returns at least one identity."""
 134          browser.get(relay["url"])
 135          time.sleep(3)
 136          h.js_async("""
 137              var cb = arguments[arguments.length - 1];
 138              window.nostr.smesh.unlockVault('testpass123')
 139                  .then(function() { cb(true); }).catch(function() { cb(true); });
 140          """)
 141          ids = h.js_async("""
 142              var cb = arguments[arguments.length - 1];
 143              window.nostr.smesh.listIdentities()
 144                  .then(function(list) { cb(list); })
 145                  .catch(function(e) { cb('[]'); });
 146          """)
 147          assert ids is not None
 148          # ids is a JSON string or parsed array depending on implementation.
 149          import json
 150          if isinstance(ids, str):
 151              ids = json.loads(ids)
 152          assert len(ids) > 0, "no identities returned"
 153  
 154      def test_get_public_key(self, relay, browser, ext, h):
 155          """NIP-07 getPublicKey returns the active identity's pubkey."""
 156          browser.get(relay["url"])
 157          time.sleep(3)
 158          pk = h.get_public_key()
 159          assert pk is not None, "getPublicKey returned null"
 160          assert len(pk) == 64, f"pubkey wrong length: {len(pk)}"
 161  
 162      def test_switch_identity(self, relay, browser, ext, h):
 163          """Switch to an identity by pubkey."""
 164          browser.get(relay["url"])
 165          time.sleep(3)
 166          pk = h.get_public_key()
 167          result = h.js_async(f"""
 168              var cb = arguments[arguments.length - 1];
 169              window.nostr.smesh.switchIdentity('{pk}')
 170                  .then(function(ok) {{ cb(ok); }})
 171                  .catch(function(e) {{ cb(false); }});
 172          """)
 173          assert result is True
 174  
 175  
 176  class TestSignerModal:
 177      """The signer modal overlay in the sm3sh app."""
 178  
 179      def test_signer_button_exists(self, relay, browser, ext, h):
 180          """The top bar has a signer button (requires being logged in)."""
 181          browser.get(relay["url"])
 182          time.sleep(2)
 183          # Fake a login by setting a pubkey in localStorage.
 184          # Use a dummy 64-char hex pubkey.
 185          dummy_pk = "a" * 64
 186          h.js(f"localStorage.setItem('smesh-pubkey', '{dummy_pk}')")
 187          browser.refresh()
 188          time.sleep(3)
 189          page_text = h.js("return document.body.innerText")
 190          assert "signer" in page_text.lower(), "signer button text not found"
 191  
 192      def test_modal_opens(self, relay, browser, ext, h):
 193          """Clicking the signer button opens the modal."""
 194          browser.get(relay["url"])
 195          time.sleep(2)
 196          h.js("localStorage.setItem('smesh-pubkey', '" + "a" * 64 + "')")
 197          browser.refresh()
 198          time.sleep(3)
 199          # Click the signer button.
 200          try:
 201              btn = browser.find_element(By.XPATH, "//button[contains(text(),'signer')]")
 202              btn.click()
 203              time.sleep(1)
 204          except Exception:
 205              pytest.skip("signer button not found in current view")
 206          # Check modal appeared.
 207          backdrop = browser.find_elements(By.CSS_SELECTOR, ".signer-backdrop")
 208          assert len(backdrop) > 0, "signer modal backdrop not found"
 209  
 210      def test_modal_closes(self, relay, browser, ext, h):
 211          """Clicking the close button dismisses the modal."""
 212          close = browser.find_elements(By.CSS_SELECTOR, ".signer-close")
 213          if close:
 214              close[0].click()
 215              time.sleep(0.5)
 216          backdrop = browser.find_elements(By.CSS_SELECTOR, ".signer-backdrop")
 217          assert len(backdrop) == 0, "modal still visible after close"
 218  
 219  
 220  class TestNIP07Signing:
 221      """NIP-07 event signing flow."""
 222  
 223      def test_sign_event(self, relay, browser, ext, h):
 224          """Sign a kind-1 event via window.nostr.signEvent."""
 225          browser.get(relay["url"])
 226          time.sleep(3)
 227          result = h.js_async("""
 228              var cb = arguments[arguments.length - 1];
 229              var ev = {
 230                  kind: 1,
 231                  created_at: Math.floor(Date.now() / 1000),
 232                  tags: [],
 233                  content: "test note from selenium"
 234              };
 235              window.nostr.signEvent(ev)
 236                  .then(function(signed) { cb(JSON.stringify(signed)); })
 237                  .catch(function(e) { cb('error:' + e.message); });
 238          """, timeout=60)
 239          assert result is not None, "signEvent returned null"
 240          import json
 241          if isinstance(result, str) and result.startswith("error:"):
 242              pytest.fail(f"signEvent error: {result}")
 243          signed = json.loads(result) if isinstance(result, str) else result
 244          assert "sig" in signed, "signed event missing sig field"
 245          assert "id" in signed, "signed event missing id field"
 246          assert "pubkey" in signed, "signed event missing pubkey field"
 247          assert len(signed["sig"]) == 128, f"sig wrong length: {len(signed['sig'])}"
 248          assert "id" in signed, "signed event missing id field"
 249          assert "pubkey" in signed, "signed event missing pubkey field"
 250          assert len(signed["sig"]) == 128, f"sig wrong length: {len(signed['sig'])}"
 251