Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slow-fans-film.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
nostream: patch
---

Add integration test coverage for NIP-04 encrypted direct messages (kind 4).
38 changes: 38 additions & 0 deletions test/integration/features/nip-04/nip-04.feature
Original file line number Diff line number Diff line change
@@ -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:"
98 changes: 98 additions & 0 deletions test/integration/features/nip-04/nip-04.feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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 { 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,
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<Record<string, any>>,
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)
await waitForEOSE(ws, subscription.name)
})

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

await new Promise<void>((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 "([^"]+)"$/, 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<OutgoingMessage>
const command = await new Promise<CommandResult>((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)
})
32 changes: 26 additions & 6 deletions test/unit/utils/messages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@ describe('createEndOfStoredEventsNoticeMessage', () => {
})
})

// NIP-20: Command Results
describe('createCommandResult', () => {
it('returns a command result message', () => {
expect(createCommandResult('event-id', true, 'accepted')).to.deep.equal([
MessageType.OK,
'event-id',
true,
'accepted',
])
})

it('returns an OK message with success=true and a reason', () => {
const eventId = 'b1601d26958e6508b7b9df0af609c652346c09392b6534d93aead9819a51b4ef'
expect(createCommandResult(eventId, true, '')).to.deep.equal([MessageType.OK, eventId, true, ''])
Expand All @@ -54,7 +62,6 @@ describe('createCommandResult', () => {
})
})

// NIP-01: Subscription messages (REQ)
describe('createSubscriptionMessage', () => {
it('returns a REQ message with a single filter', () => {
const result = createSubscriptionMessage('sub1', [{ kinds: [1] }])
Expand All @@ -71,9 +78,18 @@ describe('createSubscriptionMessage', () => {
expect(result[2]).to.deep.equal(filters[0])
expect(result[3]).to.deep.equal(filters[1])
})

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,
])
})
})

// Relayed event messages (used for event mirroring between relays)
describe('createRelayedEventMessage', () => {
let event: RelayedEvent

Expand All @@ -89,11 +105,15 @@ describe('createRelayedEventMessage', () => {
} as any
})

it('returns an EVENT message without secret when no secret is provided', () => {
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 appended when a secret is provided', () => {
expect(createRelayedEventMessage(event, 'my-secret')).to.deep.equal([MessageType.EVENT, event, 'my-secret'])
it('returns an EVENT message with secret when provided', () => {
expect(createRelayedEventMessage(event, 'shared-secret')).to.deep.equal([
MessageType.EVENT,
event,
'shared-secret',
])
})
})
Loading