From 35a466ccdbd0cf5682295dbe278346a7d7814c95 Mon Sep 17 00:00:00 2001 From: vikashsiwach Date: Mon, 20 Apr 2026 12:33:19 +0530 Subject: [PATCH] test(integration): add integration tests for NIP-02 contact lists --- .changeset/nip-02-integration-tests.md | 5 + .../features/nip-02/nip-02.feature | 26 +++ .../features/nip-02/nip-02.feature.ts | 155 ++++++++++++++++++ 3 files changed, 186 insertions(+) create mode 100644 .changeset/nip-02-integration-tests.md create mode 100644 test/integration/features/nip-02/nip-02.feature create mode 100644 test/integration/features/nip-02/nip-02.feature.ts diff --git a/.changeset/nip-02-integration-tests.md b/.changeset/nip-02-integration-tests.md new file mode 100644 index 00000000..c5dd98ce --- /dev/null +++ b/.changeset/nip-02-integration-tests.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +Add integration tests for NIP-02 contact lists (Kind 3) diff --git a/test/integration/features/nip-02/nip-02.feature b/test/integration/features/nip-02/nip-02.feature new file mode 100644 index 00000000..53afa500 --- /dev/null +++ b/test/integration/features/nip-02/nip-02.feature @@ -0,0 +1,26 @@ +Feature: NIP-02 Contact Lists + Scenario: Alice publishes a contact list + Given someone called Alice + When Alice sends a contact_list event with tags + And Alice subscribes to author Alice + Then Alice receives a contact_list event from Alice + + Scenario: Alice publishes an updated contact list + Given someone called Alice + When Alice sends a contact_list event with tags + And Alice sends a second contact_list event with different tags + And Alice subscribes to author Alice + Then Alice receives 1 contact_list event from Alice with the latest tags and EOSE + + Scenario: Tie-breaker on Identical Timestamps for contact list + Given someone called Alice + When Alice sends two identically-timestamped contact_list events where the second has a lower ID + And Alice subscribes to author Alice + Then Alice receives 1 contact_list event from Alice matching the lower ID event and EOSE + + Scenario: Bob subscribes to Alice's contact list + Given someone called Alice + And someone called Bob + When Alice sends a contact_list event with tags + And Bob subscribes to author Alice + Then Bob receives a contact_list event from Alice diff --git a/test/integration/features/nip-02/nip-02.feature.ts b/test/integration/features/nip-02/nip-02.feature.ts new file mode 100644 index 00000000..0cef81f0 --- /dev/null +++ b/test/integration/features/nip-02/nip-02.feature.ts @@ -0,0 +1,155 @@ +import { Then, When } from '@cucumber/cucumber' +import { expect } from 'chai' +import WebSocket from 'ws' + +import { createEvent, sendEvent, waitForEventCount, waitForNextEvent } from '../helpers' +import { Event } from '../../../../src/@types/event' +import { EventKinds, EventTags } from '../../../../src/constants/base' + +When( + /^(\w+) sends a contact_list event with tags$/, + async function (name: string) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + + // Create a simple contact list with a few pubkeys + const contactPubkey1 = 'a'.repeat(64) + const contactPubkey2 = 'b'.repeat(64) + + const event: Event = await createEvent( + { + pubkey, + kind: EventKinds.CONTACT_LIST, + tags: [ + [EventTags.Pubkey, contactPubkey1], + [EventTags.Pubkey, contactPubkey2], + ], + content: '', + }, + privkey, + ) + + await sendEvent(ws, event) + this.parameters.events[name].push(event) + this.parameters.contactListEvent = event + }, +) + +When( + /^(\w+) sends a second contact_list event with different tags$/, + async function (name: string) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + + // Create an updated contact list with different pubkeys + const contactPubkey3 = 'c'.repeat(64) + const contactPubkey4 = 'd'.repeat(64) + + const event: Event = await createEvent( + { + pubkey, + kind: EventKinds.CONTACT_LIST, + tags: [ + [EventTags.Pubkey, contactPubkey3], + [EventTags.Pubkey, contactPubkey4], + ], + content: '', + }, + privkey, + ) + + await sendEvent(ws, event) + this.parameters.events[name].push(event) + this.parameters.updatedContactListEvent = event + }, +) + +Then( + /^(\w+) receives a contact_list event from (\w+)$/, + async function (name: string, author: string) { + const ws = this.parameters.clients[name] as WebSocket + const subscription = this.parameters.subscriptions[name][this.parameters.subscriptions[name].length - 1] + const receivedEvent = await waitForNextEvent(ws, subscription.name) + + expect(receivedEvent.kind).to.equal(EventKinds.CONTACT_LIST) + expect(receivedEvent.pubkey).to.equal(this.parameters.identities[author].pubkey) + }, +) + +Then( + /^(\w+) receives 1 contact_list event from (\w+) with the latest tags and EOSE$/, + async function (name: 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, 1, true) + + expect(events.length).to.equal(1) + expect(events[0].kind).to.equal(EventKinds.CONTACT_LIST) + expect(events[0].pubkey).to.equal(this.parameters.identities[author].pubkey) + + // Verify it's the updated event (has the different contact pubkeys) + expect(events[0].tags).to.deep.equal(this.parameters.updatedContactListEvent.tags) + }, +) + +When( + /^(\w+) sends two identically-timestamped contact_list events where the second has a lower ID$/, + async function (name: string) { + const ws = this.parameters.clients[name] as WebSocket + const { pubkey, privkey } = this.parameters.identities[name] + + const commonTimestamp = Math.floor(Date.now() / 1000) + + const contactPubkey1 = 'e'.repeat(64) + const event1 = await createEvent( + { + pubkey, + kind: EventKinds.CONTACT_LIST, + tags: [[EventTags.Pubkey, contactPubkey1]], + content: 'first contact list', + created_at: commonTimestamp, + }, + privkey, + ) + + let nonce = 0 + let event2: Event + const contactPubkey2 = 'f'.repeat(64) + for (;;) { + event2 = await createEvent( + { + pubkey, + kind: EventKinds.CONTACT_LIST, + tags: [[EventTags.Pubkey, contactPubkey2]], + content: `second contact list ${nonce++}`, + created_at: commonTimestamp, + }, + privkey, + ) + + if (event2.id < event1.id) { + break + } + } + + await sendEvent(ws, event1) + await sendEvent(ws, event2) + + this.parameters.events[name].push(event1, event2) + this.parameters.lowerIdContactListContent = event2.tags + }, +) + +Then( + /^(\w+) receives 1 contact_list event from (\w+) matching the lower ID event and EOSE$/, + async function (name: 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, 1, true) + + expect(events.length).to.equal(1) + expect(events[0].kind).to.equal(EventKinds.CONTACT_LIST) + expect(events[0].pubkey).to.equal(this.parameters.identities[author].pubkey) + expect(events[0].tags).to.deep.equal(this.parameters.lowerIdContactListContent) + }, +)