Merge pull request #434 from v0l/nostr-package-better-tests

`nostr` package: more tests
This commit is contained in:
Kieran 2023-03-27 10:06:17 +01:00 committed by GitHub
commit d9f443d04d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 494 additions and 97 deletions

66
packages/nostr/README.md Normal file
View File

@ -0,0 +1,66 @@
# `@snort/nostr`
A strongly-typed nostr client for Node and the browser.
## NIP support
### Applicable
The goal of the project is to have all of the following implemented
and tested against a real-world relay implementation.
_Progress: 4/34 (12%)._
- [X] NIP-01: Basic protocol flow description
- [ ] NIP-02: Contact List and Petnames
- [ ] NIP-03: OpenTimestamps Attestations for Events
- [X] NIP-04: Encrypted Direct Message
- [ ] NIP-05: Mapping Nostr keys to DNS-based internet identifiers
- [ ] NIP-06: Basic key derivation from mnemonic seed phrase
- [ ] NIP-07: window.nostr capability for web browsers
- [ ] NIP-08: Handling Mentions
- [ ] NIP-09: Event Deletion
- [ ] NIP-10: Conventions for clients' use of `e` and `p` tags in text events
- TODO Check if this applies
- [ ] NIP-11: Relay Information Document
- [ ] NIP-12: Generic Tag Queries
- [ ] NIP-13: Proof of Work
- [ ] NIP-14: Subject tag in text events
- [X] NIP-15: End of Stored Events Notice
- [ ] NIP-19: bech32-encoded entities
- [X] `npub`
- [X] `nsec`
- [ ] `nprofile`
- [X] NIP-20: Command Results
- [ ] NIP-21: `nostr:` URL scheme
- [ ] NIP-23: Long-form Content
- [ ] NIP-25: Reactions
- [ ] NIP-26: Delegated Event Signing
- [ ] NIP-28: Public Chat
- [ ] NIP-36: Sensitive Content
- [ ] NIP-39: External Identities in Profiles
- [ ] NIP-40: Expiration Timestamp
- [ ] NIP-42: Authentication of clients to relays
- [ ] NIP-46: Nostr Connect
- Not sure how much of this applies, but I sure would love to see WalletConnect disappear
- [ ] NIP-50: Keywords filter
- [ ] NIP-51: Lists
- [ ] NIP-56: Reporting
- [ ] NIP-57: Lightning Zaps
- [ ] NIP-58: Badges
- [ ] NIP-65: Relay List Metadata
- [ ] NIP-78: Application-specific data
### Not Applicable
These NIPs only apply to relays and have no implications for a generic nostr client.
- NIP-16: Event Treatment
- NIP-22: Event `created_at` Limits
- NIP-33: Parameterized Replaceable Events
### Others
_If you notice an accepted NIP missing from both lists above, please [open an
issue](https://github.com/v0l/snort/issues/new?assignees=&labels=&template=feature_request.md&title=)
to let us know_.

View File

@ -1,6 +1,8 @@
version: "3.1"
services:
relay:
image: scsibug/nostr-rs-relay
build: ./relay
restart: on-failure
ports:
- 12648:8080
- 12649:8000

View File

@ -6,7 +6,7 @@
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "ts-mocha --type-check -j 1 test/*.ts",
"test": "ts-mocha --type-check -j 1 --timeout 5s test/*.ts",
"lint": "eslint ."
},
"devDependencies": {
@ -31,5 +31,12 @@
"events": "^3.3.0",
"isomorphic-ws": "^5.0.0",
"ws": "^8.12.1"
}
},
"directories": {
"test": "test"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}

View File

@ -0,0 +1 @@
node_modules/

1
packages/nostr/relay/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

@ -0,0 +1,12 @@
FROM scsibug/nostr-rs-relay
USER root
RUN apt-get update && apt-get install -y curl nodejs npm
RUN npm i -g yarn
EXPOSE 8000
COPY . .
USER $APP_USER
RUN yarn
CMD yarn app /bin/bash -c "rm -rf /usr/src/app/db/* && ./nostr-rs-relay --db /usr/src/app/db --config ./config.toml"

View File

@ -0,0 +1,4 @@
[authorization]
nip42_auth = true
# This seems to have no effect.
nip42_dms = true

View File

@ -0,0 +1,26 @@
/**
* Allows the relay to be shut down with an HTTP request, after which
* docker-compose will restart it. This allows each test to have a clean
* slate. The drawback is that the tests can't run in parallel, so the
* test suite is very slow. A better option would be to have this relay
* server manage the relay completely: star/stop isolated relay instances
* with HTTP requests and allow multiple instances to run at the same
* time so that the tests can be parallelized.
*/
import http from "node:http"
import { spawn } from "node:child_process"
const child = spawn(process.argv[2], process.argv.slice(3), {
stdio: "inherit",
})
const server = http.createServer((_, res) => {
if (!child.kill(9)) {
console.error("killing the subprocess failed")
}
res.end()
process.exit(1)
})
server.listen(8000)

View File

@ -0,0 +1,14 @@
{
"name": "relay",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"app": "ts-node index.ts"
},
"dependencies": {
"@types/node": "^18.15.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
}
}

View File

@ -1,7 +1,7 @@
import { ProtocolError } from "../error"
import { Filters, SubscriptionId } from "."
import { EventId, RawEvent } from "../event"
import WebSocket from "ws"
import WebSocket from "isomorphic-ws"
import { unixTimestamp } from "../util"
/**
@ -34,10 +34,12 @@ export class Conn {
constructor({
url,
onMessage,
onOpen,
onError,
}: {
url: URL
onMessage: (msg: IncomingMessage) => void
onOpen: () => void
onError: (err: unknown) => void
}) {
this.#onError = onError
@ -66,6 +68,7 @@ export class Conn {
this.send(msg)
}
this.#pending = []
onOpen()
})
this.#socket.addEventListener("error", (err) => {
@ -106,8 +109,9 @@ export type IncomingMessage =
| IncomingNotice
| IncomingOk
| IncomingEose
| IncomingAuth
export type IncomingKind = "event" | "notice" | "ok" | "eose"
export type IncomingKind = "event" | "notice" | "ok" | "eose" | "auth"
/**
* Incoming "EVENT" message.
@ -144,6 +148,13 @@ export interface IncomingEose {
subscriptionId: SubscriptionId
}
/**
* Incoming "AUTH" message.
*/
export interface IncomingAuth {
kind: "auth"
}
/**
* A message sent from the client to a relay.
*/
@ -305,6 +316,14 @@ async function parseIncomingMessage(data: string): Promise<IncomingMessage> {
}
}
// TODO This is incomplete
// Handle incoming "AUTH" messages.
if (json[0] === "AUTH") {
return {
kind: "auth",
}
}
throw new ProtocolError(`unknown incoming message: ${data}`)
}

View File

@ -11,6 +11,7 @@ export class EventEmitter extends Base {
eventName: "removeListener",
listener: RemoveListener
): this
override addListener(eventName: "open", listener: OpenListener): this
override addListener(eventName: "event", listener: EventListener): this
override addListener(eventName: "notice", listener: NoticeListener): this
override addListener(eventName: "ok", listener: OkListener): this
@ -22,6 +23,7 @@ export class EventEmitter extends Base {
override emit(eventName: "newListener", listener: NewListener): boolean
override emit(eventName: "removeListener", listener: RemoveListener): boolean
override emit(eventName: "open", relay: URL, nostr: Nostr): boolean
override emit(eventName: "event", params: EventParams, nostr: Nostr): boolean
override emit(eventName: "notice", notice: string, nostr: Nostr): boolean
override emit(eventName: "ok", params: OkParams, nostr: Nostr): boolean
@ -41,6 +43,7 @@ export class EventEmitter extends Base {
override listeners(eventName: "newListener"): EventListener[]
override listeners(eventName: "removeListener"): EventListener[]
override listeners(eventName: "open"): OpenListener[]
override listeners(eventName: "event"): EventListener[]
override listeners(eventName: "notice"): NoticeListener[]
override listeners(eventName: "ok"): OkListener[]
@ -52,6 +55,7 @@ export class EventEmitter extends Base {
override off(eventName: "newListener", listener: NewListener): this
override off(eventName: "removeListener", listener: RemoveListener): this
override off(eventName: "open", listener: OpenListener): this
override off(eventName: "event", listener: EventListener): this
override off(eventName: "notice", listener: NoticeListener): this
override off(eventName: "ok", listener: OkListener): this
@ -63,6 +67,7 @@ export class EventEmitter extends Base {
override on(eventName: "newListener", listener: NewListener): this
override on(eventName: "removeListener", listener: RemoveListener): this
override on(eventName: "open", listener: OpenListener): this
override on(eventName: "event", listener: EventListener): this
override on(eventName: "notice", listener: NoticeListener): this
override on(eventName: "ok", listener: OkListener): this
@ -74,6 +79,7 @@ export class EventEmitter extends Base {
override once(eventName: "newListener", listener: NewListener): this
override once(eventName: "removeListener", listener: RemoveListener): this
override once(eventName: "open", listener: OpenListener): this
override once(eventName: "event", listener: EventListener): this
override once(eventName: "notice", listener: NoticeListener): this
override once(eventName: "ok", listener: OkListener): this
@ -91,6 +97,7 @@ export class EventEmitter extends Base {
eventName: "removeListener",
listener: RemoveListener
): this
override prependListener(eventName: "open", listener: OpenListener): this
override prependListener(eventName: "event", listener: EventListener): this
override prependListener(eventName: "notice", listener: NoticeListener): this
override prependListener(eventName: "ok", listener: OkListener): this
@ -108,6 +115,7 @@ export class EventEmitter extends Base {
eventName: "removeListener",
listener: RemoveListener
): this
override prependOnceListener(eventName: "open", listener: OpenListener): this
override prependOnceListener(
eventName: "event",
listener: EventListener
@ -135,6 +143,7 @@ export class EventEmitter extends Base {
eventName: "removeListener",
listener: RemoveListener
): this
override removeListener(eventName: "open", listener: OpenListener): this
override removeListener(eventName: "event", listener: EventListener): this
override removeListener(eventName: "notice", listener: NoticeListener): this
override removeListener(eventName: "ok", listener: OkListener): this
@ -152,9 +161,11 @@ export class EventEmitter extends Base {
// emitter[Symbol.for('nodejs.rejection')](err, eventName[, ...args]) shenanigans?
}
// TODO Refactor the params to be a single interface
type EventName =
| "newListener"
| "removeListener"
| "open"
| "event"
| "notice"
| "ok"
@ -163,6 +174,7 @@ type EventName =
type NewListener = (eventName: EventName, listener: Listener) => void
type RemoveListener = (eventName: EventName, listener: Listener) => void
type OpenListener = (relay: URL, nostr: Nostr) => void
type EventListener = (params: EventParams, nostr: Nostr) => void
type NoticeListener = (notice: string, nostr: Nostr) => void
type OkListener = (params: OkParams, nostr: Nostr) => void
@ -172,6 +184,7 @@ type ErrorListener = (error: unknown, nostr: Nostr) => void
type Listener =
| NewListener
| RemoveListener
| OpenListener
| EventListener
| NoticeListener
| OkListener

View File

@ -78,13 +78,17 @@ export class Nostr extends EventEmitter {
)
} else if (msg.kind === "eose") {
this.emit("eose", msg.subscriptionId, this)
} else if (msg.kind === "auth") {
// TODO This is incomplete
} else {
throw new ProtocolError(`invalid message ${msg}`)
throw new ProtocolError(`invalid message ${JSON.stringify(msg)}`)
}
} catch (err) {
this.emit("error", err, this)
}
},
// Forward "open" events.
onOpen: () => this.emit("open", connUrl, this),
// Forward errors on this connection.
onError: (err) => this.emit("error", err, this),
})

View File

@ -104,7 +104,7 @@ export async function aesEncryptBase64(
plaintext: string
): Promise<AesEncryptedBase64> {
const sharedPoint = secp.getSharedSecret(sender, "02" + recipient)
const sharedKey = sharedPoint.slice(2, 33)
const sharedKey = sharedPoint.slice(1, 33)
if (typeof window === "object") {
const key = await window.crypto.subtle.importKey(
"raw",
@ -141,7 +141,7 @@ export async function aesEncryptBase64(
)
let encrypted = cipher.update(plaintext, "utf8", "base64")
// TODO Could save an allocation here by avoiding the +=
encrypted += cipher.final()
encrypted += cipher.final("base64")
return {
data: encrypted,
iv: Buffer.from(iv.buffer).toString("base64"),
@ -155,7 +155,7 @@ export async function aesDecryptBase64(
{ data, iv }: AesEncryptedBase64
): Promise<string> {
const sharedPoint = secp.getSharedSecret(recipient, "02" + sender)
const sharedKey = sharedPoint.slice(2, 33)
const sharedKey = sharedPoint.slice(1, 33)
if (typeof window === "object") {
// TODO Can copy this from the legacy code
throw new Error("todo")

View File

@ -5,10 +5,12 @@ import {
sha256,
schnorrSign,
schnorrVerify,
parsePublicKey,
aesDecryptBase64,
getPublicKey,
HexOrBechPrivateKey,
parsePrivateKey,
aesEncryptBase64,
} from "./crypto"
import { defined, unixTimestamp } from "./util"
@ -64,7 +66,7 @@ interface DirectMessage extends RawEvent {
/**
* Get the message plaintext, or undefined if this client is not the recipient.
*/
getMessage(recipient: PrivateKey): Promise<string | undefined>
getMessage(priv?: HexOrBechPrivateKey): Promise<string | undefined>
/**
* Get the recipient pubkey.
*/
@ -129,7 +131,7 @@ export async function signEvent<T extends Event | RawEvent>(
if (priv !== undefined) {
priv = parsePrivateKey(priv)
event.pubkey = getPublicKey(priv)
const id = await calculateEventId(event as UnsignedWithPubkey<T>)
const id = await serializeEventId(event as UnsignedWithPubkey<T>)
event.id = id
event.sig = await schnorrSign(id, priv)
return event as T
@ -158,16 +160,45 @@ export function createSetMetadata(
}
}
// TODO This is incomplete
// TODO Since you already have the private key, maybe this should return the message already signed?
// Think about this more
// Perhaps the best option is for all these factory methods to have an overload which also accept a private
// key as last parameter and return the event already signed, whereas for this method that would be
// mandatory
// E.g. opts: { sign?: boolean | HexOrBechPrivateKey } setting sign to true should use nip07
export async function createDirectMessage({
message,
recipient,
priv,
}: {
message: string
recipient: PublicKey
priv: PrivateKey
}): Promise<Unsigned<DirectMessage>> {
recipient = parsePublicKey(recipient)
priv = parsePrivateKey(priv)
const { data, iv } = await aesEncryptBase64(priv, recipient, message)
return {
kind: EventKind.DirectMessage,
tags: [["p", recipient]],
content: `${data}?iv=${iv}`,
getMessage,
getRecipient,
getPrevious,
}
}
/**
* Parse an event from its raw format.
*/
export async function parseEvent(event: RawEvent): Promise<Event> {
// TODO Validate all the fields. Lowercase hex fields, etc. Make sure everything is correct.
if (event.id !== (await calculateEventId(event))) {
if (event.id !== (await serializeEventId(event))) {
throw new ProtocolError(
`invalid id ${event.id} for event ${JSON.stringify(
event
)}, expected ${await calculateEventId(event)}`
)}, expected ${await serializeEventId(event)}`
)
}
if (!(await schnorrVerify(event.sig, event.id, event.pubkey))) {
@ -207,7 +238,7 @@ export async function parseEvent(event: RawEvent): Promise<Event> {
}
}
async function calculateEventId(
async function serializeEventId(
event: UnsignedWithPubkey<RawEvent>
): Promise<EventId> {
// It's not defined whether JSON.stringify produces a string with whitespace stripped.
@ -237,8 +268,11 @@ function getUserMetadata(this: Unsigned<RawEvent>): UserMetadata {
async function getMessage(
this: UnsignedWithPubkey<DirectMessage>,
priv?: PrivateKey
priv?: HexOrBechPrivateKey
): Promise<string | undefined> {
if (priv !== undefined) {
priv = parsePrivateKey(priv)
}
const [data, iv] = this.content.split("?iv=")
if (data === undefined || iv === undefined) {
throw new ProtocolError(`invalid direct message content ${this.content}`)

131
packages/nostr/test/dm.ts Normal file
View File

@ -0,0 +1,131 @@
import { createDirectMessage, EventKind, signEvent } from "../src/event"
import { parsePublicKey } from "../src/crypto"
import assert from "assert"
import { setup } from "./setup"
describe("dm", async function () {
const message = "for your eyes only"
// Test that the intended recipient can receive and decrypt the direct message.
it("to intended recipient", (done) => {
setup(done).then(
({
publisher,
publisherPubkey,
publisherSecret,
subscriber,
subscriberPubkey,
subscriberSecret,
timestamp,
}) => {
// Expect the direct message.
subscriber.on(
"event",
async ({ event, subscriptionId: actualSubscriptionId }, nostr) => {
try {
assert.equal(nostr, subscriber)
assert.equal(event.kind, EventKind.DirectMessage)
assert.equal(event.pubkey, parsePublicKey(publisherPubkey))
assert.equal(actualSubscriptionId, subscriptionId)
assert.ok(event.created_at >= timestamp)
if (event.kind === EventKind.DirectMessage) {
assert.equal(
event.getRecipient(),
parsePublicKey(subscriberPubkey)
)
assert.equal(await event.getMessage(subscriberSecret), message)
}
publisher.close()
subscriber.close()
done()
} catch (e) {
done(e)
}
}
)
const subscriptionId = subscriber.subscribe([])
subscriber.on("eose", async () => {
// TODO No signEvent, do something more convenient
const event = await signEvent(
await createDirectMessage({
message,
recipient: subscriberPubkey,
priv: publisherSecret,
}),
publisherSecret
)
publisher.publish(event)
})
}
)
})
// Test that an unintended recipient still receives the direct message event, but cannot decrypt it.
it("to unintended recipient", (done) => {
setup(done).then(
({
publisher,
publisherPubkey,
publisherSecret,
subscriber,
subscriberSecret,
timestamp,
}) => {
const recipientPubkey =
"npub1u2dl3scpzuwyd45flgtm3wcjgv20j4azuzgevdpgtsvvmqzvc63sz327gc"
// Expect the direct message.
subscriber.on(
"event",
async ({ event, subscriptionId: actualSubscriptionId }, nostr) => {
try {
assert.equal(nostr, subscriber)
assert.equal(event.kind, EventKind.DirectMessage)
assert.equal(event.pubkey, parsePublicKey(publisherPubkey))
assert.equal(actualSubscriptionId, subscriptionId)
assert.ok(event.created_at >= timestamp)
if (event.kind === EventKind.DirectMessage) {
assert.equal(
event.getRecipient(),
parsePublicKey(recipientPubkey)
)
assert.strictEqual(
await event.getMessage(subscriberSecret),
undefined
)
}
publisher.close()
subscriber.close()
done()
} catch (e) {
done(e)
}
}
)
const subscriptionId = subscriber.subscribe([])
subscriber.on("eose", async () => {
// TODO No signEvent, do something more convenient
const event = await signEvent(
await createDirectMessage({
message,
recipient: recipientPubkey,
priv: publisherSecret,
}),
publisherSecret
)
publisher.publish(event)
})
}
)
})
})

View File

@ -0,0 +1,70 @@
import { Nostr } from "../src/client"
import { unixTimestamp } from "../src/util"
export interface Setup {
publisher: Nostr
publisherSecret: string
publisherPubkey: string
subscriber: Nostr
subscriberSecret: string
subscriberPubkey: string
timestamp: number
url: URL
}
export async function setup(done: jest.DoneCallback): Promise<Setup> {
await restartRelay()
const publisher = new Nostr()
const subscriber = new Nostr()
const url = new URL("ws://localhost:12648")
publisher.on("error", done)
subscriber.on("error", done)
publisher.open(url)
subscriber.open(url)
return {
publisher,
publisherSecret:
"nsec15fnff4uxlgyu79ua3l7327w0wstrd6x565cx6zze78zgkktmr8vs90j363",
publisherPubkey:
"npub1he978sxy7tgc7yfp2zra05v045kfuqnfl3gwr82jd00mzxjj9fjqzw2dg7",
subscriber,
subscriberSecret:
"nsec1fxvlyqn3rugvxwaz6dr5h8jcfn0fe0lxyp7pl4mgntxfzqr7dmgst7z9ps",
subscriberPubkey:
"npub1mtwskm558jugtj724nsgf3jf80c5adl39ttydngrn48250l6xmjqa00yxd",
timestamp: unixTimestamp(),
url,
}
}
async function restartRelay() {
// Make a request to the endpoint which will crash the process and cause it to restart.
try {
await fetch("http://localhost:12649")
} catch (e) {
// Since the process exits, an error is expected.
}
// Wait until the relay process is ready.
for (;;) {
const ok = await new Promise((resolve) => {
const nostr = new Nostr()
nostr.on("error", () => {
nostr.close()
resolve(false)
})
nostr.on("open", () => {
nostr.close()
resolve(true)
})
nostr.open("ws://localhost:12648")
})
if (ok) {
break
}
await new Promise((resolve) => setTimeout(resolve, 100))
}
}

View File

@ -1,82 +0,0 @@
import { Nostr } from "../src/client"
import { createTextNote, EventKind, signEvent } from "../src/event"
import { getPublicKey } from "../src/crypto"
import assert from "assert"
import { unixTimestamp } from "../src/util"
describe("simple communication", function () {
const secret =
"nsec1xlu55y6fqfgrq448xslt6a8j2rh7lj08hyhgs94ryq04yf6surwsjl0kzh"
const pubkey = getPublicKey(secret)
const note = "hello world"
const url = new URL("ws://localhost:12648")
const timestamp = unixTimestamp()
const publisher = new Nostr()
const subscriber = new Nostr()
beforeEach(() => {
publisher.open(url)
subscriber.open(url)
})
afterEach(() => {
publisher.close()
subscriber.close()
})
it("publish and receive", function (done) {
subscriber.on("error", done)
publisher.on("error", done)
// Expect the test event.
subscriber.on("event", ({ event }, nostr) => {
assert.equal(nostr, subscriber)
assert.equal(event.kind, EventKind.TextNote)
assert.equal(event.pubkey, pubkey)
assert.equal(event.created_at, timestamp)
assert.equal(event.content, note)
done()
})
const subscriptionId = subscriber.subscribe([])
// After the subscription event sync is done, publish the test event.
subscriber.on("eose", (id, nostr) => {
assert.equal(nostr, subscriber)
assert.equal(id, subscriptionId)
signEvent(
{
...createTextNote(note),
tags: [],
},
secret
).then((event) => publisher.publish(event))
})
})
// TODO Have a way to run the relay on-demand and then re-add this test
/*
it("publish and ok", function (done) {
signEvent(
{
...createTextNote(note),
tags: [],
},
secret
).then((event) => {
publisher.on("ok", (params, nostr) => {
assert.equal(nostr, publisher)
assert.equal(params.eventId, event.id)
assert.equal(params.relay.toString(), url.toString())
assert.equal(params.ok, true)
done()
})
publisher.on("error", done)
publisher.publish(event)
})
})
*/
})

View File

@ -0,0 +1,75 @@
import { createTextNote, EventKind, signEvent } from "../src/event"
import { parsePublicKey } from "../src/crypto"
import assert from "assert"
import { setup } from "./setup"
describe("text note", async function () {
const note = "hello world"
// Test that a text note can be published by one client and received by the other.
it("publish and receive", (done) => {
setup(done).then(
({
publisher,
publisherSecret,
publisherPubkey,
subscriber,
timestamp,
}) => {
// Expect the test event.
subscriber.on(
"event",
({ event, subscriptionId: actualSubscriptionId }, nostr) => {
assert.strictEqual(nostr, subscriber)
assert.strictEqual(event.kind, EventKind.TextNote)
assert.strictEqual(event.pubkey, parsePublicKey(publisherPubkey))
assert.strictEqual(event.created_at, timestamp)
assert.strictEqual(event.content, note)
assert.strictEqual(actualSubscriptionId, subscriptionId)
subscriber.close()
publisher.close()
done()
}
)
const subscriptionId = subscriber.subscribe([])
// After the subscription event sync is done, publish the test event.
subscriber.on("eose", (id, nostr) => {
assert.strictEqual(nostr, subscriber)
assert.strictEqual(id, subscriptionId)
// TODO No signEvent, have a convenient way to do this
signEvent(
{ ...createTextNote(note), created_at: timestamp },
publisherSecret
).then((event) => publisher.publish(event))
})
}
)
})
// Test that a client interprets an "OK" message after publishing a text note.
it("publish and ok", function (done) {
setup(done).then(({ publisher, subscriber, publisherSecret, url }) => {
// TODO No signEvent, have a convenient way to do this
signEvent(createTextNote(note), publisherSecret).then((event) => {
publisher.on("ok", (params, nostr) => {
assert.equal(nostr, publisher)
assert.equal(params.eventId, event.id)
assert.equal(params.relay.toString(), url.toString())
assert.equal(params.ok, true)
publisher.close()
subscriber.close()
done()
})
publisher.publish(event)
})
})
})
})