Files
nostr-tools/test-helpers.ts
T
Tiago Balasandfiatjaf e59909038e relay: a failed auth attempt rejects and is not cached
auth() caught a throwing signer, logged it and never settled the promise,
which stayed cached for the connection: every later auth() on that relay
got the same pending promise, so a refused challenge left auth-required
subscriptions unreported and publishes waiting forever. A relay's OK false
or a timeout stayed cached the same way.

Now a failed attempt rejects (the pool reports it through onclose and the
publish rejection as before) and the next call tries again. The automatic
path on AUTH logs the failure instead of rethrowing it.

The mock relay speaks NIP-42 for the tests.
2026-09-19 07:54:00 -03:00

141 lines
3.8 KiB
TypeScript

import { Server, WebSocket } from 'mock-socket'
import { finalizeEvent, type Event, getPublicKey, generateSecretKey } from './pure.ts'
import { matchFilters, type Filter } from './filter.ts'
export const MockWebSocketClient = WebSocket
export function buildEvent(params: Partial<Event>): Event {
return {
id: '',
kind: 1,
pubkey: '',
created_at: 0,
content: '',
tags: [],
sig: '',
...params,
}
}
let serial = 0
export class MockRelay {
private _server: Server
public url: string
public secretKeys: Uint8Array[]
public preloadedEvents: Event[]
public unresponsive: boolean = false
public authRequired: boolean = false
public challenge: string = 'mock-challenge'
constructor(url?: string | undefined) {
serial++
this.url = url ?? `wss://random.mock.relay/${serial}`
this.secretKeys = [generateSecretKey(), generateSecretKey(), generateSecretKey(), generateSecretKey()]
this.preloadedEvents = this.secretKeys.map(sk =>
finalizeEvent(
{
kind: 1,
content: 'autogenerated by relay',
created_at: Math.floor(Date.now() / 1000),
tags: [['t', 'auto']],
},
sk,
),
)
this._server = new Server(this.url)
this._server.on('connection', (conn: any) => {
let subs: { [subId: string]: { conn: any; filters: Filter[] } } = {}
let authenticated = false
if (this.authRequired) conn.send(JSON.stringify(['AUTH', this.challenge]))
conn.on('message', (message: string) => {
if (this.unresponsive) return
const data = JSON.parse(message)
switch (data[0]) {
case 'AUTH': {
let event = data[1]
const ok =
event.kind === 22242 && event.tags.some((t: string[]) => t[0] === 'challenge' && t[1] === this.challenge)
if (ok) authenticated = true
conn.send(JSON.stringify(['OK', event.id, ok, ok ? '' : 'auth-required: bad challenge']))
break
}
case 'REQ': {
let subId = data[1]
let filters = data.slice(2)
if (this.authRequired && !authenticated) {
conn.send(JSON.stringify(['CLOSED', subId, 'auth-required: you need to authenticate']))
break
}
subs[subId] = { conn, filters }
this.preloadedEvents.forEach(event => {
conn.send(JSON.stringify(['EVENT', subId, event]))
})
filters.forEach((filter: Filter) => {
const kinds = filter.kinds?.length ? filter.kinds : [1]
kinds.forEach(kind => {
this.secretKeys.forEach(sk => {
const event = finalizeEvent(
{
kind,
content: 'kind-aware autogenerated by relay',
created_at: Math.floor(Date.now() / 1000),
tags: [['t', 'auto']],
},
sk,
)
conn.send(JSON.stringify(['EVENT', subId, event]))
})
})
})
conn.send(JSON.stringify(['EOSE', subId]))
break
}
case 'CLOSE': {
let subId = data[1]
delete subs[subId]
break
}
case 'EVENT': {
let event = data[1]
conn.send(JSON.stringify(['OK', event.id, 'true']))
for (let subId in subs) {
const { filters, conn: listener } = subs[subId]
if (matchFilters(filters, event)) {
listener.send(JSON.stringify(['EVENT', subId, event]))
}
}
break
}
}
})
})
}
get authors() {
return this.secretKeys.map(getPublicKey)
}
get ids() {
return this.preloadedEvents.map(evt => evt.id)
}
}