diff options
| author | Adam Soltys <[email protected]> | 2026-03-01 18:51:16 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-03-01 18:51:16 -0800 |
| commit | c7e2383736d3ef99f069226bbafad9fbddfeb030 (patch) | |
| tree | e71947a1e627cd1a4802808661ed88866a234f81 /test | |
| parent | e3b7208ae23d7b56126b0b32b66dfa48140dc1f2 (diff) | |
| download | coinos-ui-c7e2383736d3ef99f069226bbafad9fbddfeb030.tar.gz coinos-ui-c7e2383736d3ef99f069226bbafad9fbddfeb030.zip | |
messages ui
Diffstat (limited to 'test')
| -rw-r--r-- | test/e2e/dm-subscribe.spec.ts | 139 | ||||
| -rw-r--r-- | test/unified-seed.test.ts | 41 |
2 files changed, 180 insertions, 0 deletions
diff --git a/test/e2e/dm-subscribe.spec.ts b/test/e2e/dm-subscribe.spec.ts new file mode 100644 index 00000000..70fbae76 --- /dev/null +++ b/test/e2e/dm-subscribe.spec.ts @@ -0,0 +1,139 @@ +import { expect, test } from "@playwright/test"; +import { login, bobUsername, bobPassword, aliceUsername, alicePassword } from "./helpers"; + +test("recipient receives DM via relay subscription in real-time", async ({ browser }) => { + test.setTimeout(60_000); + + // --- Set up Alice (recipient) --- + const aliceContext = await browser.newContext(); + const alicePage = await aliceContext.newPage(); + + const aliceConsole: string[] = []; + alicePage.on("console", (msg) => aliceConsole.push(`[${msg.type()}] ${msg.text()}`)); + + // Track WebSocket frames on Alice's relay connections + const aliceWsFrames: { url: string; dir: string; text: string; time: number }[] = []; + alicePage.on("websocket", (ws) => { + const url = ws.url(); + console.log(`[alice-ws] connected: ${url}`); + ws.on("framesent", (frame) => { + aliceWsFrames.push({ url, dir: "sent", text: frame.payload.toString(), time: Date.now() }); + }); + ws.on("framereceived", (frame) => { + aliceWsFrames.push({ url, dir: "recv", text: frame.payload.toString(), time: Date.now() }); + }); + }); + + await login(alicePage, aliceUsername, alicePassword); + await alicePage.goto(`/dm?username=${bobUsername}`); + await alicePage.waitForLoadState("domcontentloaded"); + + const aliceTextarea = alicePage.locator("#message-contents"); + await expect(aliceTextarea).toBeVisible({ timeout: 15_000 }); + + // Wait for subscription to be fully established (EOSE received) + await alicePage.waitForTimeout(3_000); + + // Verify subscription REQ was sent correctly (NOT double-nested) + const subReqs = aliceWsFrames.filter( + (f) => f.dir === "sent" && f.text.includes('"REQ"') && f.text.includes("1059") && f.text.includes('"since"'), + ); + console.log(`[alice] Subscription REQs: ${subReqs.length}`); + for (const req of subReqs) { + console.log(`[alice] ${req.url}: ${req.text.slice(0, 200)}`); + // Verify filter is NOT double-nested (the bug was [[{...}]] instead of [{...}]) + const parsed = JSON.parse(req.text); + // REQ format: ["REQ", subId, filter1, filter2, ...] + // Filter should be an object, not an array + expect(typeof parsed[2], "Filter should be an object, not double-nested array").toBe("object"); + expect(Array.isArray(parsed[2]), "Filter should not be an array").toBeFalsy(); + } + expect(subReqs.length, "Should have at least one subscription REQ").toBeGreaterThanOrEqual(1); + + // Verify EOSE was received for the subscription + const subEoses = aliceWsFrames.filter( + (f) => f.dir === "recv" && f.text.includes('"EOSE"'), + ); + console.log(`[alice] EOSE messages: ${subEoses.length}`); + expect(subEoses.length, "Should have received EOSE").toBeGreaterThanOrEqual(1); + + // Record the timestamp before Bob sends + const beforeSend = Date.now(); + + // --- Set up Bob (sender) --- + const bobContext = await browser.newContext(); + const bobPage = await bobContext.newPage(); + + const bobWsFrames: string[] = []; + bobPage.on("websocket", (ws) => { + ws.on("framereceived", (frame) => bobWsFrames.push(frame.payload.toString())); + }); + + await login(bobPage, bobUsername, bobPassword); + await bobPage.goto(`/dm?username=${aliceUsername}`); + await bobPage.waitForLoadState("domcontentloaded"); + + const bobTextarea = bobPage.locator("#message-contents"); + await expect(bobTextarea).toBeVisible({ timeout: 15_000 }); + + const sendButton = bobPage.locator("#send-message"); + await expect(sendButton).toBeEnabled({ timeout: 5_000 }); + + // --- Bob sends a message --- + const testMessage = `realtime-test-${Date.now()}`; + console.log(`[bob] Sending: ${testMessage}`); + await bobTextarea.fill(testMessage); + await sendButton.click(); + await expect(bobTextarea).toHaveValue("", { timeout: 15_000 }); + console.log("[bob] Message sent"); + + // Verify Bob's relay accepted the events + const bobAccepts = bobWsFrames.filter((m) => m.includes('"OK"') && m.includes(",true,")); + console.log(`[bob] Relay accepted ${bobAccepts.length} events`); + expect(bobAccepts.length, "Relay should accept at least 1 event").toBeGreaterThanOrEqual(1); + + // --- Check if Alice's subscription received the new event --- + // Wait a few seconds for the relay to deliver the event to Alice + await alicePage.waitForTimeout(5_000); + + // Look for new EVENT messages received by Alice AFTER Bob sent + const newEvents = aliceWsFrames.filter( + (f) => + f.dir === "recv" && + f.text.includes('"EVENT"') && + f.time > beforeSend, + ); + console.log(`[alice] New EVENT frames received after Bob sent: ${newEvents.length}`); + for (const ev of newEvents) { + console.log(`[alice] ${ev.url}: ${ev.text.slice(0, 200)}`); + } + + // Check specifically for kind 1059 events (gift wraps) + const newGiftWraps = newEvents.filter((f) => { + try { + const parsed = JSON.parse(f.text); + return parsed[2]?.kind === 1059; + } catch { + return false; + } + }); + console.log(`[alice] New kind-1059 gift wraps received: ${newGiftWraps.length}`); + + // The subscription should deliver at least one new gift-wrapped event + expect( + newGiftWraps.length, + "Alice's subscription should receive Bob's gift-wrapped message from the relay", + ).toBeGreaterThanOrEqual(1); + + // Log any decrypt attempts from subscription handler + const subDecryptLogs = aliceConsole.filter( + (l) => l.includes("subscription event"), + ); + if (subDecryptLogs.length) { + console.log("[alice] Subscription decrypt attempts:"); + for (const l of subDecryptLogs) console.log(`[alice] ${l}`); + } + + await bobContext.close(); + await aliceContext.close(); +}); diff --git a/test/unified-seed.test.ts b/test/unified-seed.test.ts index 4410dbea..1bdf35d3 100644 --- a/test/unified-seed.test.ts +++ b/test/unified-seed.test.ts @@ -10,6 +10,8 @@ import { import { wordlist } from "@scure/bip39/wordlists/english.js"; import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js"; import { encrypt, decrypt } from "nostr-tools/nip49"; +import { privateKeyFromSeedWords } from "nostr-tools/nip06"; +import { getPublicKey } from "nostr-tools"; const versions = { private: 0x04358394, @@ -217,3 +219,42 @@ describe("backward compatibility", () => { expect(() => decrypt(encrypted, "wrongpassword")).toThrow(); }); }); + +describe("NIP-06 nostr key derivation", () => { + const mnemonic = generateMnemonic(wordlist); + + it("derives a deterministic nostr key from mnemonic via NIP-06 path", () => { + const sk = privateKeyFromSeedWords(mnemonic); + const pubkey = getPublicKey(sk); + + expect(sk).toBeInstanceOf(Uint8Array); + expect(sk.length).toBe(32); + expect(pubkey).toMatch(/^[0-9a-f]{64}$/); + + // Same mnemonic produces same key + const sk2 = privateKeyFromSeedWords(mnemonic); + expect(bytesToHex(sk2)).toBe(bytesToHex(sk)); + }); + + it("derives nostr key from entropy round-trip", () => { + const entropy = mnemonicToEntropy(mnemonic, wordlist); + const recovered = entropyToMnemonic(entropy, wordlist); + expect(recovered).toBe(mnemonic); + + const sk = privateKeyFromSeedWords(mnemonic); + const skRecovered = privateKeyFromSeedWords(recovered); + expect(bytesToHex(skRecovered)).toBe(bytesToHex(sk)); + }); + + it("nostr NIP-06 key differs from Bitcoin and Ark keys", async () => { + const seed = await mnemonicToSeed(mnemonic, password); + const master = HDKey.fromMasterSeed(seed, versions); + + const btcKey = bytesToHex(master.derive("m/84'/0'/0'").privateKey!); + const arkKey = bytesToHex(master.derive("m/86'/0'/0'/0/0").privateKey!); + const nostrKey = bytesToHex(privateKeyFromSeedWords(mnemonic)); + + expect(nostrKey).not.toBe(btcKey); + expect(nostrKey).not.toBe(arkKey); + }); +}); |
