From 92caa3f68baebdfe5688d4f30ea413662e06b016 Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Mon, 20 Apr 2026 00:27:42 +0530 Subject: [PATCH 1/3] test(integration): add NIP-62 vanish tests and fix test enviroment --- .changeset/nip-62-integration-tests.md | 5 ++ .../features/nip-62/nip-62.feature | 15 ++++++ .../features/nip-62/nip-62.feature.ts | 49 +++++++++++++++++++ test/integration/features/shared.ts | 42 +++++++++++++--- 4 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 .changeset/nip-62-integration-tests.md create mode 100644 test/integration/features/nip-62/nip-62.feature create mode 100644 test/integration/features/nip-62/nip-62.feature.ts diff --git a/.changeset/nip-62-integration-tests.md b/.changeset/nip-62-integration-tests.md new file mode 100644 index 00000000..9009ab5f --- /dev/null +++ b/.changeset/nip-62-integration-tests.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +Add NIP-62 integration tests for Request to Vanish \ No newline at end of file diff --git a/test/integration/features/nip-62/nip-62.feature b/test/integration/features/nip-62/nip-62.feature new file mode 100644 index 00000000..1a347f12 --- /dev/null +++ b/test/integration/features/nip-62/nip-62.feature @@ -0,0 +1,15 @@ +Feature: NIP-62 + Scenario: Alice requests to vanish + Given someone called Alice + And someone called Bob + And Alice sends a set_metadata event + And Alice sends a text_note event with content "please forget this" + When Alice sends a request_to_vanish event + And Bob subscribes to author Alice + Then Bob receives 1 request_to_vanish event from Alice and EOSE + + Scenario: Alice cannot publish after requesting to vanish + Given someone called Alice + When Alice sends a request_to_vanish event + And Alice drafts a text_note event with content "I should be blocked" + Then Alice sends their last draft event unsuccessfully because "blocked: request to vanish active for pubkey" diff --git a/test/integration/features/nip-62/nip-62.feature.ts b/test/integration/features/nip-62/nip-62.feature.ts new file mode 100644 index 00000000..fe540bbd --- /dev/null +++ b/test/integration/features/nip-62/nip-62.feature.ts @@ -0,0 +1,49 @@ +import { Then, When } from '@cucumber/cucumber' +import { expect } from 'chai' +import WebSocket from 'ws' + +import { createEvent, sendEvent, waitForEventCount } from '../helpers' +import { ALL_RELAYS, EventKinds, EventTags } from '../../../../src/constants/base' +import { Event } from '../../../../src/@types/event' +import { isDraft } from '../shared' + +When(/^(\w+) sends a request_to_vanish event$/, async function (name: string) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + + const event: Event = await createEvent( + { pubkey, kind: EventKinds.REQUEST_TO_VANISH, content: '', tags: [[EventTags.Relay, ALL_RELAYS]] }, + privkey, + ) + + await sendEvent(ws, event) + this.parameters.events[name].push(event) +}) + +Then( + /(\w+) receives (\d+) request_to_vanish events? from (\w+) and EOSE$/, + async function (name: string, count: string, author: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1] + const events = await waitForEventCount(ws, subscription.name, Number(count), true) + + expect(events.length).to.equal(Number(count)) + expect(events[0].kind).to.equal(EventKinds.REQUEST_TO_VANISH) + expect(events[0].pubkey).to.equal(this.parameters.identities[author].pubkey) + }, +) + +Then( + /^(\w+) sends their last draft event unsuccessfully because "([^"]+)"$/, + async function (name: string, reason: string) { + const ws = this.parameters.clients[name] as WebSocket + const event = this.parameters.events[name].findLast((event: Event) => event[isDraft]) + + delete event[isDraft] + + const command = await sendEvent(ws, event, false) + expect(command[1]).to.equal(event.id) + expect(command[2]).to.be.false + expect(command[3]).to.equal(reason) + }, +) diff --git a/test/integration/features/shared.ts b/test/integration/features/shared.ts index 71153a20..dc7ca85f 100644 --- a/test/integration/features/shared.ts +++ b/test/integration/features/shared.ts @@ -1,4 +1,4 @@ -import { After, AfterAll, Before, BeforeAll, Given, Then, When, World } from '@cucumber/cucumber' +import { After, AfterAll, Before, BeforeAll, Given, Then, When, World, setDefaultTimeout } from '@cucumber/cucumber' import { assocPath, pipe } from 'ramda' import { fromEvent, map, Observable, ReplaySubject, Subject, takeUntil } from 'rxjs' import WebSocket, { MessageEvent } from 'ws' @@ -15,19 +15,32 @@ import { workerFactory } from '../../../src/factories/worker-factory' export const isDraft = Symbol('draft') let worker: AppWorker - let dbClient: DatabaseClient let rrDbClient: DatabaseClient export const streams = new WeakMap>() -BeforeAll({ timeout: 1000 }, async function () { +setDefaultTimeout(30000) + +BeforeAll(async function () { process.env.RELAY_PORT = '18808' process.env.SECRET = Math.random().toString().repeat(6) + + process.env.DB_HOST ??= 'localhost' + process.env.DB_PORT ??= '5432' + process.env.DB_USER ??= 'postgres' + process.env.DB_PASSWORD ??= 'postgres' + process.env.DB_NAME ??= 'nostr_ts_relay_test' + process.env.DB_MIN_POOL_SIZE ??= '1' + process.env.DB_MAX_POOL_SIZE ??= '2' + process.env.DB_ACQUIRE_CONNECTION_TIMEOUT ??= '10000' + dbClient = getMasterDbClient() rrDbClient = getReadReplicaDbClient() + await dbClient.raw('SELECT 1=1') await rrDbClient.raw('SELECT 1=1') + Sinon.stub(SettingsStatic, 'watchSettings') const settings = SettingsStatic.createSettings() @@ -45,9 +58,16 @@ BeforeAll({ timeout: 1000 }, async function () { }) AfterAll({ timeout: 30000 }, async function () { + const clients = [...new Set([dbClient, rrDbClient].filter(Boolean))] + await new Promise((resolve) => { + if (!worker) { + void Promise.all(clients.map((client) => client.destroy())).then(() => resolve()) + return + } + worker.close(async () => { - await Promise.all([dbClient.destroy(), rrDbClient.destroy()]) + await Promise.all(clients.map((client) => client.destroy())) resolve() }) }) @@ -63,11 +83,13 @@ Before(function () { After(async function () { this.parameters.events = {} this.parameters.subscriptions = {} + for (const ws of Object.values(this.parameters.clients as Record)) { if (ws && ws.readyState === WebSocket.OPEN) { ws.close() } } + this.parameters.clients = {} await dbClient('events') @@ -78,20 +100,22 @@ After(async function () { ), ) .delete() + this.parameters.identities = {} }) Given(/someone called (\w+)/, async function (name: string) { const connection = await connect(name) + this.parameters.identities[name] = this.parameters.identities[name] ?? createIdentity(name) this.parameters.clients[name] = connection this.parameters.subscriptions[name] = [] this.parameters.events[name] = [] + const close = new Subject() connection.once('close', close.next.bind(close)) const projection = (raw: MessageEvent) => JSON.parse(raw.data.toString('utf8')) - const replaySubject = new ReplaySubject(2, 1000) fromEvent(connection, 'message') @@ -104,7 +128,12 @@ Given(/someone called (\w+)/, async function (name: string) { When(/(\w+) subscribes to author (\w+)$/, async function (this: World>, from: string, to: string) { const ws = this.parameters.clients[from] as WebSocket const pubkey = this.parameters.identities[to].pubkey - const subscription = { name: `test-${Math.random()}`, filters: [{ authors: [pubkey] }] } + + const subscription = { + name: `test-${Math.random()}`, + filters: [{ authors: [pubkey] }], + } + this.parameters.subscriptions[from].push(subscription) await createSubscription(ws, subscription.name, subscription.filters) @@ -113,6 +142,7 @@ When(/(\w+) subscribes to author (\w+)$/, async function (this: World((resolve, reject) => { ws.send(JSON.stringify(['CLOSE', subscription.name]), (err) => (err ? reject(err) : resolve())) }) From cf4b68d3c92371e257b42e9283cf23fb24e24c98 Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Mon, 20 Apr 2026 02:04:35 +0530 Subject: [PATCH 2/3] fix(redis-adapter): replace leftover debug calls with logger --- src/adapters/redis-adapter.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/adapters/redis-adapter.ts b/src/adapters/redis-adapter.ts index cc1f722f..3b8e062f 100644 --- a/src/adapters/redis-adapter.ts +++ b/src/adapters/redis-adapter.ts @@ -98,19 +98,19 @@ export class RedisAdapter implements ICacheAdapter { public async deleteKey(key: string): Promise { await this.connection - debug('delete %s key', key) + logger('delete %s key', key) return this.client.del(key) } public async getHKey(key: string, field: string): Promise { await this.connection - debug('get %s field for key %s', field, key) + logger('get %s field for key %s', field, key) return await this.client.hGet(key, field) ?? '' } public async setHKey(key: string, fields: Record): Promise { await this.connection - debug('set %s key', key) + logger('set %s key', key) return await this.client.hSet(key, fields) >= 0 } From 035f8b23fddab9c28fce465e35f1b2b1e795f13c Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Mon, 20 Apr 2026 03:22:01 +0530 Subject: [PATCH 3/3] test(integration): improve NIP-62 assertions --- .../features/nip-62/nip-62.feature.ts | 18 +++++--- test/integration/features/shared.ts | 42 +++---------------- 2 files changed, 19 insertions(+), 41 deletions(-) diff --git a/test/integration/features/nip-62/nip-62.feature.ts b/test/integration/features/nip-62/nip-62.feature.ts index fe540bbd..ec2ac45b 100644 --- a/test/integration/features/nip-62/nip-62.feature.ts +++ b/test/integration/features/nip-62/nip-62.feature.ts @@ -25,11 +25,15 @@ Then( async function (name: string, count: string, author: string) { const ws = this.parameters.clients[name] as WebSocket const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1] - const events = await waitForEventCount(ws, subscription.name, Number(count), true) - - expect(events.length).to.equal(Number(count)) - expect(events[0].kind).to.equal(EventKinds.REQUEST_TO_VANISH) - expect(events[0].pubkey).to.equal(this.parameters.identities[author].pubkey) + const expectedCount = Number(count) + const expectedPubkey = this.parameters.identities[author].pubkey + const events = await waitForEventCount(ws, subscription.name, expectedCount, true) + + expect(events.length).to.equal(expectedCount) + for (const event of events) { + expect(event.kind).to.equal(EventKinds.REQUEST_TO_VANISH) + expect(event.pubkey).to.equal(expectedPubkey) + } }, ) @@ -39,6 +43,10 @@ Then( const ws = this.parameters.clients[name] as WebSocket const event = this.parameters.events[name].findLast((event: Event) => event[isDraft]) + if (!event) { + throw new Error(`No draft event found for ${name}`) + } + delete event[isDraft] const command = await sendEvent(ws, event, false) diff --git a/test/integration/features/shared.ts b/test/integration/features/shared.ts index dc7ca85f..71153a20 100644 --- a/test/integration/features/shared.ts +++ b/test/integration/features/shared.ts @@ -1,4 +1,4 @@ -import { After, AfterAll, Before, BeforeAll, Given, Then, When, World, setDefaultTimeout } from '@cucumber/cucumber' +import { After, AfterAll, Before, BeforeAll, Given, Then, When, World } from '@cucumber/cucumber' import { assocPath, pipe } from 'ramda' import { fromEvent, map, Observable, ReplaySubject, Subject, takeUntil } from 'rxjs' import WebSocket, { MessageEvent } from 'ws' @@ -15,32 +15,19 @@ import { workerFactory } from '../../../src/factories/worker-factory' export const isDraft = Symbol('draft') let worker: AppWorker + let dbClient: DatabaseClient let rrDbClient: DatabaseClient export const streams = new WeakMap>() -setDefaultTimeout(30000) - -BeforeAll(async function () { +BeforeAll({ timeout: 1000 }, async function () { process.env.RELAY_PORT = '18808' process.env.SECRET = Math.random().toString().repeat(6) - - process.env.DB_HOST ??= 'localhost' - process.env.DB_PORT ??= '5432' - process.env.DB_USER ??= 'postgres' - process.env.DB_PASSWORD ??= 'postgres' - process.env.DB_NAME ??= 'nostr_ts_relay_test' - process.env.DB_MIN_POOL_SIZE ??= '1' - process.env.DB_MAX_POOL_SIZE ??= '2' - process.env.DB_ACQUIRE_CONNECTION_TIMEOUT ??= '10000' - dbClient = getMasterDbClient() rrDbClient = getReadReplicaDbClient() - await dbClient.raw('SELECT 1=1') await rrDbClient.raw('SELECT 1=1') - Sinon.stub(SettingsStatic, 'watchSettings') const settings = SettingsStatic.createSettings() @@ -58,16 +45,9 @@ BeforeAll(async function () { }) AfterAll({ timeout: 30000 }, async function () { - const clients = [...new Set([dbClient, rrDbClient].filter(Boolean))] - await new Promise((resolve) => { - if (!worker) { - void Promise.all(clients.map((client) => client.destroy())).then(() => resolve()) - return - } - worker.close(async () => { - await Promise.all(clients.map((client) => client.destroy())) + await Promise.all([dbClient.destroy(), rrDbClient.destroy()]) resolve() }) }) @@ -83,13 +63,11 @@ Before(function () { After(async function () { this.parameters.events = {} this.parameters.subscriptions = {} - for (const ws of Object.values(this.parameters.clients as Record)) { if (ws && ws.readyState === WebSocket.OPEN) { ws.close() } } - this.parameters.clients = {} await dbClient('events') @@ -100,22 +78,20 @@ After(async function () { ), ) .delete() - this.parameters.identities = {} }) Given(/someone called (\w+)/, async function (name: string) { const connection = await connect(name) - this.parameters.identities[name] = this.parameters.identities[name] ?? createIdentity(name) this.parameters.clients[name] = connection this.parameters.subscriptions[name] = [] this.parameters.events[name] = [] - const close = new Subject() connection.once('close', close.next.bind(close)) const projection = (raw: MessageEvent) => JSON.parse(raw.data.toString('utf8')) + const replaySubject = new ReplaySubject(2, 1000) fromEvent(connection, 'message') @@ -128,12 +104,7 @@ Given(/someone called (\w+)/, async function (name: string) { When(/(\w+) subscribes to author (\w+)$/, async function (this: World>, from: string, to: string) { const ws = this.parameters.clients[from] as WebSocket const pubkey = this.parameters.identities[to].pubkey - - const subscription = { - name: `test-${Math.random()}`, - filters: [{ authors: [pubkey] }], - } - + const subscription = { name: `test-${Math.random()}`, filters: [{ authors: [pubkey] }] } this.parameters.subscriptions[from].push(subscription) await createSubscription(ws, subscription.name, subscription.filters) @@ -142,7 +113,6 @@ When(/(\w+) subscribes to author (\w+)$/, async function (this: World((resolve, reject) => { ws.send(JSON.stringify(['CLOSE', subscription.name]), (err) => (err ? reject(err) : resolve())) })