mirror of
https://github.com/block/buzz.git
synced 2026-11-12 04:12:21 +00:00
261 lines
9.4 KiB
JavaScript
261 lines
9.4 KiB
JavaScript
/**
|
||
* sprout-proxy E2E test using nostr-tools v2.23.3
|
||
*
|
||
* nostr-tools is the most widely-used Nostr library in the ecosystem,
|
||
* powering Coracle, Snort, Damus Web, and hundreds of other clients.
|
||
*
|
||
* Tests the full NIP-28 channel flow through sprout-proxy:
|
||
* 1. NIP-42 authentication (automatic via onauth callback)
|
||
* 2. Channel discovery (kind:40)
|
||
* 3. Channel metadata (kind:41)
|
||
* 4. Send a channel message (kind:42 via NIP-28 channelMessageEvent)
|
||
* 5. Receive messages (kind:42 subscription)
|
||
* 6. Live streaming (real-time event delivery)
|
||
*/
|
||
|
||
import { Relay } from 'nostr-tools/relay'
|
||
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure'
|
||
import { channelMessageEvent } from 'nostr-tools/nip28'
|
||
import { hexToBytes } from 'nostr-tools/utils'
|
||
import WebSocket from 'ws'
|
||
|
||
// ── Config ────────────────────────────────────────────────────────────────────
|
||
|
||
const RELAY_URL = process.env.RELAY_URL || 'ws://localhost:4869'
|
||
const GUEST_PRIVKEY_HEX = process.env.GUEST_PRIVKEY
|
||
const CHANNEL_EVENT_ID = process.env.CHANNEL_EVENT_ID
|
||
|
||
if (!GUEST_PRIVKEY_HEX || !CHANNEL_EVENT_ID) {
|
||
console.error('Usage: GUEST_PRIVKEY=<hex> CHANNEL_EVENT_ID=<hex> node test-sprout-proxy.mjs')
|
||
process.exit(1)
|
||
}
|
||
|
||
const secretKey = hexToBytes(GUEST_PRIVKEY_HEX)
|
||
const pubkey = getPublicKey(secretKey)
|
||
|
||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||
|
||
const results = []
|
||
function pass(name, detail) {
|
||
results.push({ name, ok: true })
|
||
console.log(` ✅ ${name}${detail ? ` — ${detail}` : ''}`)
|
||
}
|
||
function fail(name, detail) {
|
||
results.push({ name, ok: false })
|
||
console.log(` ❌ ${name}${detail ? ` — ${detail}` : ''}`)
|
||
}
|
||
|
||
/** Collect events from a subscription until EOSE, with timeout. */
|
||
function list(relay, filters, timeoutMs = 8000) {
|
||
return new Promise((resolve, reject) => {
|
||
const events = []
|
||
const timer = setTimeout(() => {
|
||
sub.close()
|
||
resolve(events)
|
||
}, timeoutMs)
|
||
|
||
const sub = relay.subscribe(filters, {
|
||
onevent(evt) { events.push(evt) },
|
||
oneose() {
|
||
clearTimeout(timer)
|
||
sub.close()
|
||
resolve(events)
|
||
},
|
||
})
|
||
})
|
||
}
|
||
|
||
// ── Main ──────────────────────────────────────────────────────────────────────
|
||
|
||
async function main() {
|
||
console.log('═'.repeat(64))
|
||
console.log(' nostr-tools v2.23 E2E TEST against sprout-proxy')
|
||
console.log('═'.repeat(64))
|
||
console.log(` Relay: ${RELAY_URL}`)
|
||
console.log(` Pubkey: ${pubkey.slice(0, 24)}...`)
|
||
console.log()
|
||
|
||
// ── 1. Connect + NIP-42 Auth ──────────────────────────────────────────
|
||
|
||
console.log('[1/6] Connect + NIP-42 Authentication')
|
||
|
||
// nostr-tools' Relay class calls onauth when it receives an AUTH challenge.
|
||
// We set it before connecting so the handshake is automatic.
|
||
const relay = new Relay(RELAY_URL, { websocketImplementation: WebSocket })
|
||
|
||
// Wire up the NIP-42 auth callback BEFORE connecting.
|
||
// nostr-tools calls: relay.auth(onauth) → onauth(makeAuthEvent(url, challenge))
|
||
// So onauth receives an unsigned event template and must return a signed event.
|
||
let authResolved = false
|
||
relay.onauth = async (authEventTemplate) => {
|
||
const signed = finalizeEvent(authEventTemplate, secretKey)
|
||
authResolved = true
|
||
return signed
|
||
}
|
||
|
||
try {
|
||
await relay.connect()
|
||
} catch (e) {
|
||
fail('Connect', e.message)
|
||
process.exit(1)
|
||
}
|
||
|
||
// Wait a moment for the AUTH challenge to arrive and be handled
|
||
// nostr-tools' onauth is called synchronously when the AUTH message arrives
|
||
await new Promise(r => setTimeout(r, 1500))
|
||
|
||
if (authResolved) {
|
||
pass('NIP-42 Auth', `challenge received, signed as ${pubkey.slice(0, 16)}...`)
|
||
} else {
|
||
// Auth might still work reactively — the proxy sends CLOSED/OK auth-required
|
||
// which triggers nostr-tools to call relay.auth()
|
||
console.log(' ⏳ No proactive AUTH challenge yet — testing reactive auth...')
|
||
}
|
||
|
||
// ── 2. Channel Discovery (kind:40) ────────────────────────────────────
|
||
|
||
console.log('\n[2/6] Channel Discovery (kind:40)')
|
||
|
||
try {
|
||
const channels = await list(relay, [{ kinds: [40], limit: 100 }])
|
||
if (channels.length > 0) {
|
||
for (const evt of channels) {
|
||
try {
|
||
const c = JSON.parse(evt.content)
|
||
console.log(` • ${c.name || '(unnamed)'} (id: ${evt.id.slice(0, 24)}...)`)
|
||
} catch { console.log(` • (id: ${evt.id.slice(0, 24)}...)`) }
|
||
}
|
||
pass('Channel Discovery', `${channels.length} channel(s)`)
|
||
} else {
|
||
fail('Channel Discovery', 'no kind:40 events returned')
|
||
}
|
||
} catch (e) {
|
||
fail('Channel Discovery', e.message)
|
||
}
|
||
|
||
// ── 3. Channel Metadata (kind:41) ─────────────────────────────────────
|
||
|
||
console.log('\n[3/6] Channel Metadata (kind:41)')
|
||
|
||
try {
|
||
const meta = await list(relay, [{ kinds: [41], '#e': [CHANNEL_EVENT_ID], limit: 5 }])
|
||
if (meta.length > 0) {
|
||
const c = JSON.parse(meta[0].content)
|
||
pass('Channel Metadata', `name="${c.name}", about="${(c.about || '').slice(0, 50)}"`)
|
||
} else {
|
||
fail('Channel Metadata', 'no kind:41 events')
|
||
}
|
||
} catch (e) {
|
||
fail('Channel Metadata', e.message)
|
||
}
|
||
|
||
// ── 4. Send Channel Message (kind:42 via NIP-28) ──────────────────────
|
||
|
||
console.log('\n[4/6] Send Channel Message (NIP-28 channelMessageEvent)')
|
||
|
||
const ts = Math.floor(Date.now() / 1000)
|
||
const msgText = `Hello from nostr-tools! 📦 [${ts}]`
|
||
|
||
let sentEvent
|
||
try {
|
||
sentEvent = channelMessageEvent({
|
||
channel_create_event_id: CHANNEL_EVENT_ID,
|
||
relay_url: RELAY_URL,
|
||
content: msgText,
|
||
created_at: ts,
|
||
}, secretKey)
|
||
|
||
await relay.publish(sentEvent)
|
||
pass('Send Message', `"${msgText}" (id: ${sentEvent.id.slice(0, 16)}...)`)
|
||
} catch (e) {
|
||
fail('Send Message', e.message)
|
||
}
|
||
|
||
// ── 5. Read Messages (kind:42) ────────────────────────────────────────
|
||
|
||
console.log('\n[5/6] Read Messages (kind:42 query)')
|
||
|
||
try {
|
||
const msgs = await list(relay, [{ kinds: [42], '#e': [CHANNEL_EVENT_ID], limit: 10 }])
|
||
let foundOurs = false
|
||
console.log(` ${msgs.length} message(s):`)
|
||
for (const evt of msgs) {
|
||
const marker = evt.content.includes('nostr-tools') ? '→ ' : ' '
|
||
console.log(` ${marker}[${evt.pubkey.slice(0, 12)}...] ${evt.content.slice(0, 65)}`)
|
||
if (evt.content.includes('nostr-tools')) foundOurs = true
|
||
}
|
||
if (foundOurs) {
|
||
pass('Read Messages', `found our message among ${msgs.length}`)
|
||
} else if (msgs.length > 0) {
|
||
pass('Read Messages', `${msgs.length} messages (ours may be propagating)`)
|
||
} else {
|
||
fail('Read Messages', 'no messages')
|
||
}
|
||
} catch (e) {
|
||
fail('Read Messages', e.message)
|
||
}
|
||
|
||
// ── 6. Live Streaming ─────────────────────────────────────────────────
|
||
|
||
console.log('\n[6/6] Live Streaming (real-time event delivery)')
|
||
|
||
try {
|
||
const streamResult = await new Promise((resolve) => {
|
||
const timer = setTimeout(() => { sub.close(); resolve(false) }, 8000)
|
||
|
||
const sub = relay.subscribe([{
|
||
kinds: [42],
|
||
'#e': [CHANNEL_EVENT_ID],
|
||
since: ts,
|
||
}], {
|
||
onevent(evt) {
|
||
if (evt.content.includes('stream-ping')) {
|
||
clearTimeout(timer)
|
||
sub.close()
|
||
resolve(true)
|
||
}
|
||
},
|
||
oneose() {
|
||
// After EOSE, publish a new message that should arrive live
|
||
const ping = channelMessageEvent({
|
||
channel_create_event_id: CHANNEL_EVENT_ID,
|
||
relay_url: RELAY_URL,
|
||
content: `nostr-tools stream-ping ${Date.now()}`,
|
||
created_at: Math.floor(Date.now() / 1000),
|
||
}, secretKey)
|
||
relay.publish(ping).catch(() => {})
|
||
},
|
||
})
|
||
})
|
||
|
||
if (streamResult) {
|
||
pass('Live Streaming', 'received real-time event')
|
||
} else {
|
||
fail('Live Streaming', 'timed out')
|
||
}
|
||
} catch (e) {
|
||
fail('Live Streaming', e.message)
|
||
}
|
||
|
||
// ── Summary ───────────────────────────────────────────────────────────
|
||
|
||
relay.close()
|
||
|
||
console.log('\n' + '═'.repeat(64))
|
||
console.log(' RESULTS')
|
||
console.log('═'.repeat(64))
|
||
let allPass = true
|
||
for (const r of results) {
|
||
console.log(` ${r.ok ? '✅ PASS' : '❌ FAIL'} ${r.name}`)
|
||
if (!r.ok) allPass = false
|
||
}
|
||
console.log('═'.repeat(64))
|
||
console.log(allPass
|
||
? ' 🎉 ALL TESTS PASSED — nostr-tools works with sprout-proxy!'
|
||
: ' ⚠️ Some tests failed')
|
||
console.log('═'.repeat(64))
|
||
process.exit(allPass ? 0 : 1)
|
||
}
|
||
|
||
main().catch(e => { console.error('Fatal:', e); process.exit(1) })
|