From 908aceaa065698da53721d9a8a87dd068961324d Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Wed, 22 Apr 2026 06:27:59 +0530 Subject: [PATCH 1/5] test: add NIP-04 integration tests --- .../features/nip-04/nip-04.feature | 38 ++++++++++ .../features/nip-04/nip-04.feature.ts | 76 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 test/integration/features/nip-04/nip-04.feature create mode 100644 test/integration/features/nip-04/nip-04.feature.ts diff --git a/test/integration/features/nip-04/nip-04.feature b/test/integration/features/nip-04/nip-04.feature new file mode 100644 index 00000000..a5003e68 --- /dev/null +++ b/test/integration/features/nip-04/nip-04.feature @@ -0,0 +1,38 @@ +Feature: NIP-04 Encrypted direct messages + Scenario: Alice publishes an encrypted direct message to Bob + Given someone called Alice + And someone called Bob + When Alice sends an encrypted_direct_message event with content "ciphertext-for-bob" to Bob + And Alice subscribes to author Alice + Then Alice receives an encrypted_direct_message event from Alice with content "ciphertext-for-bob" tagged for Bob + + Scenario: Alice gets her encrypted direct message by event ID + Given someone called Alice + And someone called Bob + When Alice sends an encrypted_direct_message event with content "ciphertext-by-id" to Bob + And Alice subscribes to last event from Alice + Then Alice receives an encrypted_direct_message event from Alice with content "ciphertext-by-id" tagged for Bob + + Scenario: Bob receives Alice's encrypted direct message through #p filter + Given someone called Alice + And someone called Bob + When Alice sends an encrypted_direct_message event with content "ciphertext-for-bob-filter" to Bob + And Bob subscribes to tag p with Bob pubkey + Then Bob receives an encrypted_direct_message event from Alice with content "ciphertext-for-bob-filter" tagged for Bob + + Scenario: Bob and Charlie receive identical ciphertext for Bob's #p filter + Given someone called Alice + And someone called Bob + And someone called Charlie + And Bob subscribes to tag p with Bob pubkey + And Charlie subscribes to tag p with Bob pubkey + When Alice sends an encrypted_direct_message event with content "ciphertext-visible-to-filter-subscribers" to Bob + Then Bob receives an encrypted_direct_message event from Alice with content "ciphertext-visible-to-filter-subscribers" tagged for Bob + And Charlie receives an encrypted_direct_message event from Alice with content "ciphertext-visible-to-filter-subscribers" tagged for Bob + + Scenario: Alice submits a duplicate encrypted direct message + Given someone called Alice + And someone called Bob + When Alice sends an encrypted_direct_message event with content "ciphertext-duplicate" to Bob + And Alice resubmits their last event + Then Alice receives a successful command result with message "duplicate:" diff --git a/test/integration/features/nip-04/nip-04.feature.ts b/test/integration/features/nip-04/nip-04.feature.ts new file mode 100644 index 00000000..94084330 --- /dev/null +++ b/test/integration/features/nip-04/nip-04.feature.ts @@ -0,0 +1,76 @@ +import { Then, When, World } from '@cucumber/cucumber' +import { expect } from 'chai' +import WebSocket from 'ws' + +import { createEvent, createSubscription, sendEvent, waitForNextEvent } from '../helpers' +import { EventKinds, EventTags } from '../../../../src/constants/base' +import { CommandResult } from '../../../../src/@types/messages' +import { Event } from '../../../../src/@types/event' + +When(/^(\w+) sends an encrypted_direct_message event with content "([^"]+)" to (\w+)$/, async function( + name: string, + content: string, + recipient: string, +) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + const recipientPubkey = this.parameters.identities[recipient].pubkey + + const event: Event = await createEvent( + { + pubkey, + kind: EventKinds.ENCRYPTED_DIRECT_MESSAGE, + content, + tags: [[EventTags.Pubkey, recipientPubkey]], + }, + privkey, + ) + + await sendEvent(ws, event) + this.parameters.events[name].push(event) +}) + +When(/^(\w+) subscribes to tag p with (\w+) pubkey$/, async function( + this: World>, + name: string, + target: string, +) { + const ws = this.parameters.clients[name] as WebSocket + const targetPubkey = this.parameters.identities[target].pubkey + const subscription = { name: `test-${Math.random()}`, filters: [{ '#p': [targetPubkey] }] } + this.parameters.subscriptions[name].push(subscription) + + await createSubscription(ws, subscription.name, subscription.filters) +}) + +Then(/(\w+) receives an encrypted_direct_message event from (\w+) with content "([^"]+?)" tagged for (\w+)/, async function( + name: string, + author: string, + content: string, + recipient: string, +) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1] + const recipientPubkey = this.parameters.identities[recipient].pubkey + const receivedEvent = await waitForNextEvent(ws, subscription.name, content) + + expect(receivedEvent.kind).to.equal(EventKinds.ENCRYPTED_DIRECT_MESSAGE) + expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey) + expect(receivedEvent.content).to.equal(content) + expect(receivedEvent.tags).to.deep.include([EventTags.Pubkey, recipientPubkey]) +}) + +When(/^(\w+) resubmits their last event$/, async function(name: string) { + const ws = this.parameters.clients[name] as WebSocket + const event = this.parameters.events[name][this.parameters.events[name].length - 1] as Event + const command = await sendEvent(ws, event) as CommandResult + this.parameters.commands = this.parameters.commands ?? {} + this.parameters.commands[name] = command +}) + +Then(/^(\w+) receives a successful command result with message "([^"]+)"$/, function(name: string, message: string) { + const command = this.parameters.commands[name] as CommandResult + + expect(command[2]).to.equal(true) + expect(command[3]).to.equal(message) +}) From 036fe7cdd3fa3ffbad52694ca4407c7db5b31fcb Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Wed, 22 Apr 2026 06:34:39 +0530 Subject: [PATCH 2/5] test: wait for EOSE in NIP-04 p-tag subscription step --- test/integration/features/nip-04/nip-04.feature.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/integration/features/nip-04/nip-04.feature.ts b/test/integration/features/nip-04/nip-04.feature.ts index 94084330..a5b42bf8 100644 --- a/test/integration/features/nip-04/nip-04.feature.ts +++ b/test/integration/features/nip-04/nip-04.feature.ts @@ -2,7 +2,7 @@ import { Then, When, World } from '@cucumber/cucumber' import { expect } from 'chai' import WebSocket from 'ws' -import { createEvent, createSubscription, sendEvent, waitForNextEvent } from '../helpers' +import { createEvent, createSubscription, sendEvent, waitForEOSE, waitForNextEvent } from '../helpers' import { EventKinds, EventTags } from '../../../../src/constants/base' import { CommandResult } from '../../../../src/@types/messages' import { Event } from '../../../../src/@types/event' @@ -41,6 +41,7 @@ When(/^(\w+) subscribes to tag p with (\w+) pubkey$/, async function( this.parameters.subscriptions[name].push(subscription) await createSubscription(ws, subscription.name, subscription.filters) + await waitForEOSE(ws, subscription.name) }) Then(/(\w+) receives an encrypted_direct_message event from (\w+) with content "([^"]+?)" tagged for (\w+)/, async function( From 2d0a7c4f57e91c7054bb9fbd9114885e793aa9cb Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Wed, 22 Apr 2026 06:46:19 +0530 Subject: [PATCH 3/5] test: harden duplicate command assertion in NIP-04 flow --- .../features/nip-04/nip-04.feature.ts | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/test/integration/features/nip-04/nip-04.feature.ts b/test/integration/features/nip-04/nip-04.feature.ts index a5b42bf8..f6b7de75 100644 --- a/test/integration/features/nip-04/nip-04.feature.ts +++ b/test/integration/features/nip-04/nip-04.feature.ts @@ -1,11 +1,13 @@ import { Then, When, World } from '@cucumber/cucumber' import { expect } from 'chai' +import { Observable } from 'rxjs' import WebSocket from 'ws' +import { CommandResult, MessageType, OutgoingMessage } from '../../../../src/@types/messages' import { createEvent, createSubscription, sendEvent, waitForEOSE, waitForNextEvent } from '../helpers' import { EventKinds, EventTags } from '../../../../src/constants/base' -import { CommandResult } from '../../../../src/@types/messages' import { Event } from '../../../../src/@types/event' +import { streams } from '../shared' When(/^(\w+) sends an encrypted_direct_message event with content "([^"]+)" to (\w+)$/, async function( name: string, @@ -64,13 +66,32 @@ Then(/(\w+) receives an encrypted_direct_message event from (\w+) with content " When(/^(\w+) resubmits their last event$/, async function(name: string) { const ws = this.parameters.clients[name] as WebSocket const event = this.parameters.events[name][this.parameters.events[name].length - 1] as Event - const command = await sendEvent(ws, event) as CommandResult - this.parameters.commands = this.parameters.commands ?? {} - this.parameters.commands[name] = command + + await new Promise((resolve, reject) => { + ws.send(JSON.stringify(['EVENT', event]), (err?: Error) => err ? reject(err) : resolve()) + }) + + this.parameters.lastResubmittedEventId = this.parameters.lastResubmittedEventId ?? {} + this.parameters.lastResubmittedEventId[name] = event.id }) -Then(/^(\w+) receives a successful command result with message "([^"]+)"$/, function(name: string, message: string) { - const command = this.parameters.commands[name] as CommandResult +Then(/^(\w+) receives a successful command result with message "([^"]+)"$/, async function(name: string, message: string) { + const ws = this.parameters.clients[name] as WebSocket + const eventId = this.parameters.lastResubmittedEventId[name] as string + const observable = streams.get(ws) as Observable + const command = await new Promise((resolve, reject) => { + observable.subscribe((response: OutgoingMessage) => { + if ( + response[0] === MessageType.OK && + response[1] === eventId && + response[3] === message + ) { + resolve(response) + } else if (response[0] === MessageType.NOTICE) { + reject(new Error(response[1])) + } + }) + }) expect(command[2]).to.equal(true) expect(command[3]).to.equal(message) From 568c86e924a6c260f70d68080476b89a17d6d475 Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Wed, 22 Apr 2026 07:03:26 +0530 Subject: [PATCH 4/5] chore: add empty changeset for NIP-04 test coverage --- .changeset/slow-fans-film.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changeset/slow-fans-film.md diff --git a/.changeset/slow-fans-film.md b/.changeset/slow-fans-film.md new file mode 100644 index 00000000..aba17a30 --- /dev/null +++ b/.changeset/slow-fans-film.md @@ -0,0 +1,4 @@ +--- +--- + +Add integration test coverage for NIP-04 encrypted direct messages (kind 4). From 00393df205faa1f366454409eec9d9fecffbd9d8 Mon Sep 17 00:00:00 2001 From: Priyanshubhartistm Date: Wed, 22 Apr 2026 07:49:52 +0530 Subject: [PATCH 5/5] test: fix changeset metadata and add messages util tests --- .changeset/slow-fans-film.md | 1 + test/unit/utils/messages.spec.ts | 52 ++++++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/.changeset/slow-fans-film.md b/.changeset/slow-fans-film.md index aba17a30..b13e339b 100644 --- a/.changeset/slow-fans-film.md +++ b/.changeset/slow-fans-film.md @@ -1,4 +1,5 @@ --- +nostream: patch --- Add integration test coverage for NIP-04 encrypted direct messages (kind 4). diff --git a/test/unit/utils/messages.spec.ts b/test/unit/utils/messages.spec.ts index 461a991a..68149b8f 100644 --- a/test/unit/utils/messages.spec.ts +++ b/test/unit/utils/messages.spec.ts @@ -1,7 +1,14 @@ import { expect } from 'chai' -import { createEndOfStoredEventsNoticeMessage, createNoticeMessage, createOutgoingEventMessage } from '../../../src/utils/messages' -import { Event } from '../../../src/@types/event' +import { + createCommandResult, + createEndOfStoredEventsNoticeMessage, + createNoticeMessage, + createOutgoingEventMessage, + createRelayedEventMessage, + createSubscriptionMessage, +} from '../../../src/utils/messages' +import { Event, RelayedEvent } from '../../../src/@types/event' import { MessageType } from '../../../src/@types/messages' describe('createNotice', () => { @@ -25,3 +32,44 @@ describe('createEndOfStoredEventsNoticeMessage', () => { }) }) +describe('createCommandResult', () => { + it('returns a command result message', () => { + expect(createCommandResult('event-id', true, 'accepted')).to.deep.equal([ + MessageType.OK, + 'event-id', + true, + 'accepted', + ]) + }) +}) + +describe('createSubscriptionMessage', () => { + it('returns a subscription message with filters', () => { + const filters = [{ authors: ['author-1'], kinds: [1], '#p': ['recipient-1'] }] + + expect(createSubscriptionMessage('subscriptionId', filters)).to.deep.equal([ + MessageType.REQ, + 'subscriptionId', + ...filters, + ]) + }) +}) + +describe('createRelayedEventMessage', () => { + const event: RelayedEvent = { + id: 'event-id', + } as any + + it('returns an EVENT message without secret when secret is missing', () => { + expect(createRelayedEventMessage(event)).to.deep.equal([MessageType.EVENT, event]) + }) + + it('returns an EVENT message with secret when provided', () => { + expect(createRelayedEventMessage(event, 'shared-secret')).to.deep.equal([ + MessageType.EVENT, + event, + 'shared-secret', + ]) + }) +}) +