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