Skip to content

Commit de14f3c

Browse files
test: add NIP-04 integration coverage for encrypted direct messages (#562)
1 parent bdd4f6b commit de14f3c

4 files changed

Lines changed: 167 additions & 6 deletions

File tree

.changeset/slow-fans-film.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
nostream: patch
3+
---
4+
5+
Add integration test coverage for NIP-04 encrypted direct messages (kind 4).
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
Feature: NIP-04 Encrypted direct messages
2+
Scenario: Alice publishes an encrypted direct message to Bob
3+
Given someone called Alice
4+
And someone called Bob
5+
When Alice sends an encrypted_direct_message event with content "ciphertext-for-bob" to Bob
6+
And Alice subscribes to author Alice
7+
Then Alice receives an encrypted_direct_message event from Alice with content "ciphertext-for-bob" tagged for Bob
8+
9+
Scenario: Alice gets her encrypted direct message by event ID
10+
Given someone called Alice
11+
And someone called Bob
12+
When Alice sends an encrypted_direct_message event with content "ciphertext-by-id" to Bob
13+
And Alice subscribes to last event from Alice
14+
Then Alice receives an encrypted_direct_message event from Alice with content "ciphertext-by-id" tagged for Bob
15+
16+
Scenario: Bob receives Alice's encrypted direct message through #p filter
17+
Given someone called Alice
18+
And someone called Bob
19+
When Alice sends an encrypted_direct_message event with content "ciphertext-for-bob-filter" to Bob
20+
And Bob subscribes to tag p with Bob pubkey
21+
Then Bob receives an encrypted_direct_message event from Alice with content "ciphertext-for-bob-filter" tagged for Bob
22+
23+
Scenario: Bob and Charlie receive identical ciphertext for Bob's #p filter
24+
Given someone called Alice
25+
And someone called Bob
26+
And someone called Charlie
27+
And Bob subscribes to tag p with Bob pubkey
28+
And Charlie subscribes to tag p with Bob pubkey
29+
When Alice sends an encrypted_direct_message event with content "ciphertext-visible-to-filter-subscribers" to Bob
30+
Then Bob receives an encrypted_direct_message event from Alice with content "ciphertext-visible-to-filter-subscribers" tagged for Bob
31+
And Charlie receives an encrypted_direct_message event from Alice with content "ciphertext-visible-to-filter-subscribers" tagged for Bob
32+
33+
Scenario: Alice submits a duplicate encrypted direct message
34+
Given someone called Alice
35+
And someone called Bob
36+
When Alice sends an encrypted_direct_message event with content "ciphertext-duplicate" to Bob
37+
And Alice resubmits their last event
38+
Then Alice receives a successful command result with message "duplicate:"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Then, When, World } from '@cucumber/cucumber'
2+
import { expect } from 'chai'
3+
import { Observable } from 'rxjs'
4+
import WebSocket from 'ws'
5+
6+
import { CommandResult, MessageType, OutgoingMessage } from '../../../../src/@types/messages'
7+
import { createEvent, createSubscription, sendEvent, waitForEOSE, waitForNextEvent } from '../helpers'
8+
import { EventKinds, EventTags } from '../../../../src/constants/base'
9+
import { Event } from '../../../../src/@types/event'
10+
import { streams } from '../shared'
11+
12+
When(/^(\w+) sends an encrypted_direct_message event with content "([^"]+)" to (\w+)$/, async function(
13+
name: string,
14+
content: string,
15+
recipient: string,
16+
) {
17+
const ws = this.parameters.clients[name] as WebSocket
18+
const { pubkey, privkey } = this.parameters.identities[name]
19+
const recipientPubkey = this.parameters.identities[recipient].pubkey
20+
21+
const event: Event = await createEvent(
22+
{
23+
pubkey,
24+
kind: EventKinds.ENCRYPTED_DIRECT_MESSAGE,
25+
content,
26+
tags: [[EventTags.Pubkey, recipientPubkey]],
27+
},
28+
privkey,
29+
)
30+
31+
await sendEvent(ws, event)
32+
this.parameters.events[name].push(event)
33+
})
34+
35+
When(/^(\w+) subscribes to tag p with (\w+) pubkey$/, async function(
36+
this: World<Record<string, any>>,
37+
name: string,
38+
target: string,
39+
) {
40+
const ws = this.parameters.clients[name] as WebSocket
41+
const targetPubkey = this.parameters.identities[target].pubkey
42+
const subscription = { name: `test-${Math.random()}`, filters: [{ '#p': [targetPubkey] }] }
43+
this.parameters.subscriptions[name].push(subscription)
44+
45+
await createSubscription(ws, subscription.name, subscription.filters)
46+
await waitForEOSE(ws, subscription.name)
47+
})
48+
49+
Then(/(\w+) receives an encrypted_direct_message event from (\w+) with content "([^"]+?)" tagged for (\w+)/, async function(
50+
name: string,
51+
author: string,
52+
content: string,
53+
recipient: string,
54+
) {
55+
const ws = this.parameters.clients[name] as WebSocket
56+
const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1]
57+
const recipientPubkey = this.parameters.identities[recipient].pubkey
58+
const receivedEvent = await waitForNextEvent(ws, subscription.name, content)
59+
60+
expect(receivedEvent.kind).to.equal(EventKinds.ENCRYPTED_DIRECT_MESSAGE)
61+
expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey)
62+
expect(receivedEvent.content).to.equal(content)
63+
expect(receivedEvent.tags).to.deep.include([EventTags.Pubkey, recipientPubkey])
64+
})
65+
66+
When(/^(\w+) resubmits their last event$/, async function(name: string) {
67+
const ws = this.parameters.clients[name] as WebSocket
68+
const event = this.parameters.events[name][this.parameters.events[name].length - 1] as Event
69+
70+
await new Promise<void>((resolve, reject) => {
71+
ws.send(JSON.stringify(['EVENT', event]), (err?: Error) => err ? reject(err) : resolve())
72+
})
73+
74+
this.parameters.lastResubmittedEventId = this.parameters.lastResubmittedEventId ?? {}
75+
this.parameters.lastResubmittedEventId[name] = event.id
76+
})
77+
78+
Then(/^(\w+) receives a successful command result with message "([^"]+)"$/, async function(name: string, message: string) {
79+
const ws = this.parameters.clients[name] as WebSocket
80+
const eventId = this.parameters.lastResubmittedEventId[name] as string
81+
const observable = streams.get(ws) as Observable<OutgoingMessage>
82+
const command = await new Promise<CommandResult>((resolve, reject) => {
83+
observable.subscribe((response: OutgoingMessage) => {
84+
if (
85+
response[0] === MessageType.OK &&
86+
response[1] === eventId &&
87+
response[3] === message
88+
) {
89+
resolve(response)
90+
} else if (response[0] === MessageType.NOTICE) {
91+
reject(new Error(response[1]))
92+
}
93+
})
94+
})
95+
96+
expect(command[2]).to.equal(true)
97+
expect(command[3]).to.equal(message)
98+
})

test/unit/utils/messages.spec.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,16 @@ describe('createEndOfStoredEventsNoticeMessage', () => {
3636
})
3737
})
3838

39-
// NIP-20: Command Results
4039
describe('createCommandResult', () => {
40+
it('returns a command result message', () => {
41+
expect(createCommandResult('event-id', true, 'accepted')).to.deep.equal([
42+
MessageType.OK,
43+
'event-id',
44+
true,
45+
'accepted',
46+
])
47+
})
48+
4149
it('returns an OK message with success=true and a reason', () => {
4250
const eventId = 'b1601d26958e6508b7b9df0af609c652346c09392b6534d93aead9819a51b4ef'
4351
expect(createCommandResult(eventId, true, '')).to.deep.equal([MessageType.OK, eventId, true, ''])
@@ -54,7 +62,6 @@ describe('createCommandResult', () => {
5462
})
5563
})
5664

57-
// NIP-01: Subscription messages (REQ)
5865
describe('createSubscriptionMessage', () => {
5966
it('returns a REQ message with a single filter', () => {
6067
const result = createSubscriptionMessage('sub1', [{ kinds: [1] }])
@@ -71,9 +78,18 @@ describe('createSubscriptionMessage', () => {
7178
expect(result[2]).to.deep.equal(filters[0])
7279
expect(result[3]).to.deep.equal(filters[1])
7380
})
81+
82+
it('returns a subscription message with filters', () => {
83+
const filters = [{ authors: ['author-1'], kinds: [1], '#p': ['recipient-1'] }]
84+
85+
expect(createSubscriptionMessage('subscriptionId', filters)).to.deep.equal([
86+
MessageType.REQ,
87+
'subscriptionId',
88+
...filters,
89+
])
90+
})
7491
})
7592

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

@@ -89,11 +105,15 @@ describe('createRelayedEventMessage', () => {
89105
} as any
90106
})
91107

92-
it('returns an EVENT message without secret when no secret is provided', () => {
108+
it('returns an EVENT message without secret when secret is missing', () => {
93109
expect(createRelayedEventMessage(event)).to.deep.equal([MessageType.EVENT, event])
94110
})
95111

96-
it('returns an EVENT message with secret appended when a secret is provided', () => {
97-
expect(createRelayedEventMessage(event, 'my-secret')).to.deep.equal([MessageType.EVENT, event, 'my-secret'])
112+
it('returns an EVENT message with secret when provided', () => {
113+
expect(createRelayedEventMessage(event, 'shared-secret')).to.deep.equal([
114+
MessageType.EVENT,
115+
event,
116+
'shared-secret',
117+
])
98118
})
99119
})

0 commit comments

Comments
 (0)