Skip to content

Commit a8336ec

Browse files
authored
Merge branch 'main' into feat/fix-whitelist-matching
2 parents ae5e00d + 4d030c7 commit a8336ec

8 files changed

Lines changed: 180 additions & 4 deletions

File tree

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 NIP-11 integration tests and fix max_filters mapping in relay information document.

.changeset/strong-candles-yell.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+
Dedup keys were taking multiple tags, that was not according to NIP-01 behaviour.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
exports.up = async function (knex) {
2+
await knex.raw(`
3+
WITH ranked AS (
4+
SELECT
5+
id,
6+
row_number() OVER (
7+
PARTITION BY event_pubkey, event_kind, jsonb_build_array(COALESCE(event_deduplication->>0, ''))
8+
ORDER BY event_created_at DESC, event_id ASC
9+
) AS row_rank
10+
FROM events
11+
WHERE event_kind >= 30000
12+
AND event_kind < 40000
13+
)
14+
DELETE FROM events AS e
15+
USING ranked AS r
16+
WHERE e.id = r.id
17+
AND r.row_rank > 1;
18+
`)
19+
20+
await knex.raw(`
21+
UPDATE events
22+
SET event_deduplication = jsonb_build_array(COALESCE(event_deduplication->>0, ''))
23+
WHERE event_kind >= 30000
24+
AND event_kind < 40000
25+
AND event_deduplication IS DISTINCT FROM jsonb_build_array(COALESCE(event_deduplication->>0, ''));
26+
`)
27+
}
28+
29+
exports.down = async function () {
30+
// Irreversible data migration.
31+
}

src/handlers/request-handlers/root-request-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const rootRequestHandler = (request: Request, response: Response, next: N
3636
limitation: {
3737
max_message_length: settings.network.maxPayloadSize,
3838
max_subscriptions: settings.limits?.client?.subscription?.maxSubscriptions,
39-
max_filters: settings.limits?.client?.subscription?.maxFilterValues,
39+
max_filters: settings.limits?.client?.subscription?.maxFilters,
4040
max_limit: settings.limits?.client?.subscription?.maxLimit,
4141
max_subid_length: settings.limits?.client?.subscription?.maxSubscriptionIdLength,
4242
min_prefix: settings.limits?.client?.subscription?.minPrefixLength,

src/services/event-import-service.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ const enrichEventMetadata = (event: Event): Event => {
2626
}
2727

2828
if (isParameterizedReplaceableEvent(event)) {
29-
const [, ...deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [
29+
const [, deduplication] = event.tags.find((tag) => tag.length >= 2 && tag[0] === EventTags.Deduplication) ?? [
3030
null,
3131
'',
3232
]
33-
enriched = { ...enriched, [EventDeduplicationMetadataKey]: deduplication }
33+
enriched = { ...enriched, [EventDeduplicationMetadataKey]: deduplication ? [deduplication] : [''] }
3434
}
3535

3636
return enriched as Event
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Feature: NIP-11
2+
Scenario: Relay returns information document for NIP-11 request
3+
When a client requests the relay information document
4+
Then the response status is 200
5+
And the response Content-Type includes "application/nostr+json"
6+
And the relay information document contains the required fields
7+
8+
Scenario: Relay information document lists supported NIPs from package.json
9+
When a client requests the relay information document
10+
Then the supported_nips field matches the NIPs declared in package.json
11+
12+
Scenario: Relay does not return information document for a non-NIP-11 Accept header
13+
When a client requests the root path with Accept header "text/html"
14+
Then the response Content-Type does not include "application/nostr+json"
15+
And the response body is not a relay information document
16+
17+
Scenario: Relay information document reports max_filters from settings
18+
When a client requests the relay information document
19+
Then the limitation object contains a max_filters field
20+
21+
Scenario: WebSocket connections coexist with HTTP on the same port
22+
Given someone called Alice
23+
When Alice sends a text_note event with content "nostr is great"
24+
And Alice subscribes to author Alice
25+
Then Alice receives a text_note event from Alice with content "nostr is great"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { Then, When, World } from '@cucumber/cucumber'
2+
import axios, { AxiosResponse } from 'axios'
3+
import chai from 'chai'
4+
5+
import packageJson from '../../../../package.json'
6+
import { createSettings } from '../../../../src/factories/settings-factory'
7+
8+
chai.use(require('sinon-chai'))
9+
const { expect } = chai
10+
11+
const BASE_URL = 'http://localhost:18808'
12+
13+
When('a client requests the relay information document', async function(this: World<Record<string, any>>) {
14+
const response: AxiosResponse = await axios.get(BASE_URL, {
15+
headers: { Accept: 'application/nostr+json' },
16+
validateStatus: () => true,
17+
})
18+
this.parameters.httpResponse = response
19+
})
20+
21+
When('a client requests the root path with Accept header {string}', async function(
22+
this: World<Record<string, any>>,
23+
acceptHeader: string,
24+
) {
25+
const response: AxiosResponse = await axios.get(BASE_URL, {
26+
headers: { Accept: acceptHeader },
27+
validateStatus: () => true,
28+
})
29+
this.parameters.httpResponse = response
30+
})
31+
32+
Then('the response status is {int}', function(this: World<Record<string, any>>, status: number) {
33+
expect(this.parameters.httpResponse.status).to.equal(status)
34+
})
35+
36+
Then('the response Content-Type includes {string}', function(
37+
this: World<Record<string, any>>,
38+
contentType: string,
39+
) {
40+
expect(this.parameters.httpResponse.headers['content-type']).to.include(contentType)
41+
})
42+
43+
Then('the response Content-Type does not include {string}', function(
44+
this: World<Record<string, any>>,
45+
contentType: string,
46+
) {
47+
expect(this.parameters.httpResponse.headers['content-type']).to.not.include(contentType)
48+
})
49+
50+
Then('the relay information document contains the required fields', function(this: World<Record<string, any>>) {
51+
const doc = this.parameters.httpResponse.data
52+
for (const field of ['name', 'description', 'pubkey', 'supported_nips', 'software', 'version']) {
53+
expect(doc, `expected relay info doc to have field "${field}"`).to.have.property(field)
54+
}
55+
})
56+
57+
Then('the supported_nips field matches the NIPs declared in package.json', function(this: World<Record<string, any>>) {
58+
const doc = this.parameters.httpResponse.data
59+
expect(doc.supported_nips).to.deep.equal(packageJson.supportedNips)
60+
})
61+
62+
Then('the response body is not a relay information document', function(this: World<Record<string, any>>) {
63+
const body = this.parameters.httpResponse.data
64+
const isRelayInfoDoc = typeof body === 'object' && body !== null && 'supported_nips' in body
65+
expect(isRelayInfoDoc).to.equal(false)
66+
})
67+
68+
Then('the limitation object contains a max_filters field', function(this: World<Record<string, any>>) {
69+
const doc = this.parameters.httpResponse.data
70+
const expectedMaxFilters = createSettings().limits?.client?.subscription?.maxFilters
71+
expect(doc.limitation.max_filters).to.equal(expectedMaxFilters)
72+
})

test/unit/services/event-import-service.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { join } from 'path'
33
import fs from 'fs'
44
import os from 'os'
55

6-
import { EventImportLineError, EventImportService, EventImportStats } from '../../../src/services/event-import-service'
6+
import {
7+
createEventBatchPersister,
8+
EventImportLineError,
9+
EventImportService,
10+
EventImportStats,
11+
} from '../../../src/services/event-import-service'
12+
import { EventDeduplicationMetadataKey, EventKinds, EventTags } from '../../../src/constants/base'
713
import { Event } from '../../../src/@types/event'
814
import { expect } from 'chai'
915
import { getEvents } from '../data/events'
@@ -170,4 +176,36 @@ describe('EventImportService', () => {
170176
expect(lineErrors.length).to.equal(0)
171177
}
172178
})
179+
180+
it('normalizes parameterized replaceable deduplication to first d tag value', async () => {
181+
const parameterizedEvent: Event = {
182+
id: 'a'.repeat(64),
183+
pubkey: 'b'.repeat(64),
184+
created_at: 1,
185+
kind: EventKinds.PARAMETERIZED_REPLACEABLE_FIRST,
186+
tags: [[EventTags.Deduplication, 'one', 'two']],
187+
content: 'hello',
188+
sig: 'c'.repeat(128),
189+
}
190+
191+
let upsertedEvents: Event[] = []
192+
193+
const eventRepository = {
194+
create: async () => 0,
195+
createMany: async () => 0,
196+
upsert: async () => 0,
197+
upsertMany: async (events: Event[]) => {
198+
upsertedEvents = events
199+
return events.length
200+
},
201+
deleteByPubkeyAndIds: async () => 0,
202+
} as any
203+
204+
const persistBatch = createEventBatchPersister(eventRepository)
205+
const inserted = await persistBatch([parameterizedEvent])
206+
207+
expect(inserted).to.equal(1)
208+
expect(upsertedEvents).to.have.length(1)
209+
expect((upsertedEvents[0] as any)[EventDeduplicationMetadataKey]).to.deep.equal(['one'])
210+
})
173211
})

0 commit comments

Comments
 (0)