nostr
package: more tests
#434
@ -1,6 +1,8 @@
|
||||
version: "3.1"
|
||||
services:
|
||||
relay:
|
||||
image: scsibug/nostr-rs-relay
|
||||
build: ./relay
|
||||
restart: on-failure
|
||||
ports:
|
||||
- 12648:8080
|
||||
- 12649:8000
|
||||
|
@ -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": ""
|
||||
}
|
||||
|
1
packages/nostr/relay/.dockerignore
Normal file
1
packages/nostr/relay/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
1
packages/nostr/relay/.gitignore
vendored
Normal file
1
packages/nostr/relay/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules/
|
12
packages/nostr/relay/Dockerfile
Normal file
12
packages/nostr/relay/Dockerfile
Normal 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"
|
16
packages/nostr/relay/index.ts
Normal file
16
packages/nostr/relay/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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)
|
14
packages/nostr/relay/package.json
Normal file
14
packages/nostr/relay/package.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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) => {
|
||||
|
@ -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,12 @@ export class EventEmitter extends Base {
|
||||
// emitter[Symbol.for('nodejs.rejection')](err, eventName[, ...args]) shenanigans?
|
||||
}
|
||||
|
||||
// TODO Add an open event
|
||||
// TODO Refactor the params
|
||||
type EventName =
|
||||
| "newListener"
|
||||
| "removeListener"
|
||||
| "open"
|
||||
| "event"
|
||||
| "notice"
|
||||
| "ok"
|
||||
@ -163,6 +175,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 +185,7 @@ type ErrorListener = (error: unknown, nostr: Nostr) => void
|
||||
type Listener =
|
||||
| NewListener
|
||||
| RemoveListener
|
||||
| OpenListener
|
||||
| EventListener
|
||||
| NoticeListener
|
||||
| OkListener
|
||||
|
@ -85,6 +85,8 @@ export class Nostr extends EventEmitter {
|
||||
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),
|
||||
})
|
||||
|
@ -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")
|
||||
|
@ -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,44 @@ 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
|
||||
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 +237,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 +267,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}`)
|
||||
|
125
packages/nostr/test/dm.ts
Normal file
125
packages/nostr/test/dm.ts
Normal file
@ -0,0 +1,125 @@
|
||||
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)
|
||||
}
|
||||
|
||||
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.only("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
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
70
packages/nostr/test/setup.ts
Normal file
70
packages/nostr/test/setup.ts
Normal 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))
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
75
packages/nostr/test/text-note.ts
Normal file
75
packages/nostr/test/text-note.ts
Normal 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"
|
||||
|
||||
it("publish and receive", (done) => {
|
||||
// Test that a text note can be published by one client and received by the other.
|
||||
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))
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("publish and ok", function (done) {
|
||||
// Test that a client interprets an "OK" message after publishing a text note.
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in New Issue
Block a user