psf-bch-api 1.2.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env-local +28 -0
- package/bin/server.js +61 -9
- package/package.json +6 -2
- package/src/adapters/fulcrum-api.js +124 -0
- package/src/adapters/full-node-rpc.js +2 -6
- package/src/adapters/index.js +4 -0
- package/src/adapters/slp-indexer-api.js +124 -0
- package/src/config/env/common.js +29 -25
- package/src/config/x402.js +7 -0
- package/src/controllers/rest-api/fulcrum/controller.js +563 -0
- package/src/controllers/rest-api/fulcrum/router.js +64 -0
- package/src/controllers/rest-api/full-node/blockchain/controller.js +4 -4
- package/src/controllers/rest-api/full-node/mining/controller.js +99 -0
- package/src/controllers/rest-api/full-node/mining/router.js +52 -0
- package/src/controllers/rest-api/full-node/rawtransactions/controller.js +333 -0
- package/src/controllers/rest-api/full-node/rawtransactions/router.js +58 -0
- package/src/controllers/rest-api/index.js +23 -3
- package/src/controllers/rest-api/price/controller.js +96 -0
- package/src/controllers/rest-api/price/router.js +52 -0
- package/src/controllers/rest-api/slp/controller.js +218 -0
- package/src/controllers/rest-api/slp/router.js +55 -0
- package/src/controllers/timer-controller.js +1 -1
- package/src/middleware/basic-auth.js +61 -0
- package/src/use-cases/fulcrum-use-cases.js +155 -0
- package/src/use-cases/full-node-mining-use-cases.js +28 -0
- package/src/use-cases/full-node-rawtransactions-use-cases.js +121 -0
- package/src/use-cases/index.js +10 -0
- package/src/use-cases/price-use-cases.js +83 -0
- package/src/use-cases/slp-use-cases.js +321 -0
- package/test/unit/controllers/blockchain-controller-unit.js +2 -3
- package/test/unit/controllers/fulcrum-controller-unit.js +481 -0
- package/test/unit/controllers/mining-controller-unit.js +139 -0
- package/test/unit/controllers/price-controller-unit.js +116 -0
- package/test/unit/controllers/rawtransactions-controller-unit.js +388 -0
- package/test/unit/controllers/rest-api-index-unit.js +67 -3
- package/test/unit/controllers/slp-controller-unit.js +312 -0
- package/test/unit/use-cases/fulcrum-use-cases-unit.js +297 -0
- package/test/unit/use-cases/full-node-mining-use-cases-unit.js +84 -0
- package/test/unit/use-cases/full-node-rawtransactions-use-cases-unit.js +267 -0
- package/test/unit/use-cases/price-use-cases-unit.js +103 -0
- package/test/unit/use-cases/slp-use-cases-unit.js +296 -0
- package/src/entities/event.js +0 -71
- package/test/integration/api/event-integration.js +0 -250
- package/test/integration/api/req-integration.js +0 -173
- package/test/integration/api/subscription-integration.js +0 -198
- package/test/integration/use-cases/manage-subscription-integration.js +0 -163
- package/test/integration/use-cases/publish-event-integration.js +0 -104
- package/test/integration/use-cases/query-events-integration.js +0 -95
- package/test/unit/entities/event-unit.js +0 -139
- /package/{index.js → psf-bch-api.js} +0 -0
- /package/src/controllers/rest-api/full-node/blockchain/{index.js → router.js} +0 -0
- /package/src/controllers/rest-api/full-node/control/{index.js → router.js} +0 -0
- /package/src/controllers/rest-api/full-node/dsproof/{index.js → router.js} +0 -0
package/src/entities/event.js
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Event entity - represents a Nostr event.
|
|
3
|
-
This is a domain model following Clean Architecture principles.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
class Event {
|
|
7
|
-
constructor (data) {
|
|
8
|
-
this.id = data.id
|
|
9
|
-
this.pubkey = data.pubkey
|
|
10
|
-
this.created_at = data.created_at
|
|
11
|
-
this.kind = data.kind
|
|
12
|
-
this.tags = data.tags || []
|
|
13
|
-
this.content = data.content
|
|
14
|
-
this.sig = data.sig
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Validates the event structure
|
|
19
|
-
* @returns {boolean} True if valid
|
|
20
|
-
*/
|
|
21
|
-
isValid () {
|
|
22
|
-
if (!this.id || !this.pubkey || !this.created_at || this.kind === undefined || !this.sig) {
|
|
23
|
-
return false
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Basic type checks
|
|
27
|
-
if (typeof this.id !== 'string' || this.id.length !== 64) {
|
|
28
|
-
return false
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (typeof this.pubkey !== 'string' || this.pubkey.length !== 64) {
|
|
32
|
-
return false
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (typeof this.created_at !== 'number') {
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (typeof this.kind !== 'number' || this.kind < 0 || this.kind > 65535) {
|
|
40
|
-
return false
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (typeof this.sig !== 'string' || this.sig.length !== 128) {
|
|
44
|
-
return false
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!Array.isArray(this.tags)) {
|
|
48
|
-
return false
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return true
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Convert to plain object
|
|
56
|
-
* @returns {Object} Plain event object
|
|
57
|
-
*/
|
|
58
|
-
toJSON () {
|
|
59
|
-
return {
|
|
60
|
-
id: this.id,
|
|
61
|
-
pubkey: this.pubkey,
|
|
62
|
-
created_at: this.created_at,
|
|
63
|
-
kind: this.kind,
|
|
64
|
-
tags: this.tags,
|
|
65
|
-
content: this.content,
|
|
66
|
-
sig: this.sig
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export default Event
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Integration tests for POST /event endpoint.
|
|
3
|
-
These tests require a running Nostr relay.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// npm libraries
|
|
7
|
-
import { assert } from 'chai'
|
|
8
|
-
|
|
9
|
-
// Unit under test
|
|
10
|
-
import Server from '../../../bin/server.js'
|
|
11
|
-
import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools/pure'
|
|
12
|
-
import { hexToBytes } from '@noble/hashes/utils.js'
|
|
13
|
-
|
|
14
|
-
describe('#event-integration.js', () => {
|
|
15
|
-
let server
|
|
16
|
-
const baseUrl = 'http://localhost:3001' // Use different port for tests
|
|
17
|
-
|
|
18
|
-
before(async () => {
|
|
19
|
-
// Start test server
|
|
20
|
-
server = new Server()
|
|
21
|
-
server.config.port = 3001
|
|
22
|
-
await server.startServer()
|
|
23
|
-
|
|
24
|
-
// Wait for server to be ready
|
|
25
|
-
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
after(async () => {
|
|
29
|
-
// Stop server
|
|
30
|
-
if (server && server.server) {
|
|
31
|
-
await new Promise((resolve) => {
|
|
32
|
-
server.server.close(() => {
|
|
33
|
-
resolve()
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
describe('POST /event', () => {
|
|
40
|
-
it('should publish kind 0 event (profile metadata) - covers example 01', async () => {
|
|
41
|
-
// Generate keys
|
|
42
|
-
const sk = generateSecretKey()
|
|
43
|
-
|
|
44
|
-
// Create profile metadata event (kind 0)
|
|
45
|
-
const profileMetadata = {
|
|
46
|
-
name: 'Test User',
|
|
47
|
-
about: 'Integration test user',
|
|
48
|
-
picture: 'https://example.com/test.jpg'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const eventTemplate = {
|
|
52
|
-
kind: 0,
|
|
53
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
54
|
-
tags: [],
|
|
55
|
-
content: JSON.stringify(profileMetadata)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Sign the event
|
|
59
|
-
const signedEvent = finalizeEvent(eventTemplate, sk)
|
|
60
|
-
|
|
61
|
-
// Publish to REST API
|
|
62
|
-
const response = await fetch(`${baseUrl}/event`, {
|
|
63
|
-
method: 'POST',
|
|
64
|
-
headers: {
|
|
65
|
-
'Content-Type': 'application/json'
|
|
66
|
-
},
|
|
67
|
-
body: JSON.stringify(signedEvent)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const result = await response.json()
|
|
71
|
-
|
|
72
|
-
// Assert response
|
|
73
|
-
assert.equal(response.status, 200)
|
|
74
|
-
assert.property(result, 'accepted')
|
|
75
|
-
assert.property(result, 'eventId')
|
|
76
|
-
assert.equal(result.eventId, signedEvent.id)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('should publish kind 1 event (text post) - covers example 03', async () => {
|
|
80
|
-
// Alice's private key from examples
|
|
81
|
-
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
|
82
|
-
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
|
83
|
-
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
|
84
|
-
|
|
85
|
-
// Generate a post
|
|
86
|
-
const eventTemplate = {
|
|
87
|
-
kind: 1,
|
|
88
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
89
|
-
tags: [],
|
|
90
|
-
content: 'Integration test post'
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Sign the post
|
|
94
|
-
const signedEvent = finalizeEvent(eventTemplate, alicePrivKeyBin)
|
|
95
|
-
|
|
96
|
-
// Publish to REST API
|
|
97
|
-
const response = await fetch(`${baseUrl}/event`, {
|
|
98
|
-
method: 'POST',
|
|
99
|
-
headers: {
|
|
100
|
-
'Content-Type': 'application/json'
|
|
101
|
-
},
|
|
102
|
-
body: JSON.stringify(signedEvent)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const result = await response.json()
|
|
106
|
-
|
|
107
|
-
// Assert response
|
|
108
|
-
assert.equal(response.status, 200)
|
|
109
|
-
assert.property(result, 'accepted')
|
|
110
|
-
assert.property(result, 'eventId')
|
|
111
|
-
assert.equal(result.eventId, signedEvent.id)
|
|
112
|
-
assert.equal(signedEvent.pubkey, alicePubKey)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('should publish kind 3 event (follow list) - covers example 06', async () => {
|
|
116
|
-
// Alice's private key
|
|
117
|
-
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
|
118
|
-
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
|
119
|
-
|
|
120
|
-
// Bob's public key
|
|
121
|
-
const bobPrivKeyHex = 'd2e71a977bc3900d6b0f787421e3d1a666cd12ca625482b0d9eeffd23489c99f'
|
|
122
|
-
const bobPrivKeyBin = hexToBytes(bobPrivKeyHex)
|
|
123
|
-
const bobPubKey = getPublicKey(bobPrivKeyBin)
|
|
124
|
-
|
|
125
|
-
const psf = 'wss://nostr-relay.psfoundation.info'
|
|
126
|
-
|
|
127
|
-
const followList = [
|
|
128
|
-
['p', bobPubKey, psf, 'bob']
|
|
129
|
-
]
|
|
130
|
-
|
|
131
|
-
// Generate a follow list event (kind 3)
|
|
132
|
-
const eventTemplate = {
|
|
133
|
-
kind: 3,
|
|
134
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
135
|
-
tags: followList,
|
|
136
|
-
content: ''
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Sign the event
|
|
140
|
-
const signedEvent = finalizeEvent(eventTemplate, alicePrivKeyBin)
|
|
141
|
-
|
|
142
|
-
// Publish to REST API
|
|
143
|
-
const response = await fetch(`${baseUrl}/event`, {
|
|
144
|
-
method: 'POST',
|
|
145
|
-
headers: {
|
|
146
|
-
'Content-Type': 'application/json'
|
|
147
|
-
},
|
|
148
|
-
body: JSON.stringify(signedEvent)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
const result = await response.json()
|
|
152
|
-
|
|
153
|
-
// Assert response
|
|
154
|
-
assert.equal(response.status, 200)
|
|
155
|
-
assert.property(result, 'accepted')
|
|
156
|
-
assert.property(result, 'eventId')
|
|
157
|
-
assert.equal(result.eventId, signedEvent.id)
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
it('should publish kind 7 event (reaction/like) - covers example 07', async () => {
|
|
161
|
-
// Bob's private key
|
|
162
|
-
const bobPrivKeyHex = 'd2e71a977bc3900d6b0f787421e3d1a666cd12ca625482b0d9eeffd23489c99f'
|
|
163
|
-
const bobPrivKeyBin = hexToBytes(bobPrivKeyHex)
|
|
164
|
-
const bobPubKey = getPublicKey(bobPrivKeyBin)
|
|
165
|
-
|
|
166
|
-
const psf = 'wss://nostr-relay.psfoundation.info'
|
|
167
|
-
|
|
168
|
-
// Use a test event ID
|
|
169
|
-
const evIdToLike = 'd09b4c5da59be3cd2768aa53fa78b77bf4859084c94f3bf26d401f004a9c8167'
|
|
170
|
-
const evIdAuthorPubKey = '2c7e76c0f8dc1dca9d0197c7d19be580a8d074ccada6a2f6ebe056ae41092e92'
|
|
171
|
-
|
|
172
|
-
// Generate like event (kind 7)
|
|
173
|
-
const likeEventTemplate = {
|
|
174
|
-
kind: 7,
|
|
175
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
176
|
-
pubkey: bobPubKey,
|
|
177
|
-
tags: [
|
|
178
|
-
['e', evIdToLike, psf],
|
|
179
|
-
['p', evIdAuthorPubKey, psf]
|
|
180
|
-
],
|
|
181
|
-
content: '+'
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Sign the event
|
|
185
|
-
const signedEvent = finalizeEvent(likeEventTemplate, bobPrivKeyBin)
|
|
186
|
-
|
|
187
|
-
// Publish to REST API
|
|
188
|
-
const response = await fetch(`${baseUrl}/event`, {
|
|
189
|
-
method: 'POST',
|
|
190
|
-
headers: {
|
|
191
|
-
'Content-Type': 'application/json'
|
|
192
|
-
},
|
|
193
|
-
body: JSON.stringify(signedEvent)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
const result = await response.json()
|
|
197
|
-
|
|
198
|
-
// Assert response
|
|
199
|
-
assert.equal(response.status, 200)
|
|
200
|
-
assert.property(result, 'accepted')
|
|
201
|
-
assert.property(result, 'eventId')
|
|
202
|
-
assert.equal(result.eventId, signedEvent.id)
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('should reject invalid event', async () => {
|
|
206
|
-
const invalidEvent = {
|
|
207
|
-
id: 'invalid',
|
|
208
|
-
pubkey: 'invalid',
|
|
209
|
-
created_at: Math.floor(Date.now() / 1000),
|
|
210
|
-
kind: 1,
|
|
211
|
-
tags: [],
|
|
212
|
-
content: 'Test',
|
|
213
|
-
sig: 'invalid'
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const response = await fetch(`${baseUrl}/event`, {
|
|
217
|
-
method: 'POST',
|
|
218
|
-
headers: {
|
|
219
|
-
'Content-Type': 'application/json'
|
|
220
|
-
},
|
|
221
|
-
body: JSON.stringify(invalidEvent)
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
const result = await response.json()
|
|
225
|
-
|
|
226
|
-
// Should reject invalid event - error response format
|
|
227
|
-
assert.equal(response.status, 400)
|
|
228
|
-
assert.property(result, 'error')
|
|
229
|
-
assert.include(result.error, 'Invalid event structure')
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('should return 400 when event data is missing', async () => {
|
|
233
|
-
// Send empty body - Express will parse as undefined, controller should handle it
|
|
234
|
-
const response = await fetch(`${baseUrl}/event`, {
|
|
235
|
-
method: 'POST',
|
|
236
|
-
headers: {
|
|
237
|
-
'Content-Type': 'application/json'
|
|
238
|
-
},
|
|
239
|
-
body: ''
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
// Empty body should be parsed as undefined by Express
|
|
243
|
-
const result = await response.json()
|
|
244
|
-
|
|
245
|
-
assert.equal(response.status, 400)
|
|
246
|
-
assert.property(result, 'error')
|
|
247
|
-
assert.include(result.error, 'Event data is required')
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
})
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Integration tests for GET /req/:subId endpoint.
|
|
3
|
-
These tests require a running Nostr relay.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// npm libraries
|
|
7
|
-
import { assert } from 'chai'
|
|
8
|
-
|
|
9
|
-
// Unit under test
|
|
10
|
-
import Server from '../../../bin/server.js'
|
|
11
|
-
import { getPublicKey } from 'nostr-tools/pure'
|
|
12
|
-
import { hexToBytes } from '@noble/hashes/utils.js'
|
|
13
|
-
|
|
14
|
-
describe('#req-integration.js', () => {
|
|
15
|
-
let server
|
|
16
|
-
const baseUrl = 'http://localhost:3002' // Use different port for tests
|
|
17
|
-
|
|
18
|
-
before(async () => {
|
|
19
|
-
// Start test server
|
|
20
|
-
server = new Server()
|
|
21
|
-
server.config.port = 3002
|
|
22
|
-
await server.startServer()
|
|
23
|
-
|
|
24
|
-
// Wait for server to be ready
|
|
25
|
-
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
after(async () => {
|
|
29
|
-
// Stop server
|
|
30
|
-
if (server && server.server) {
|
|
31
|
-
await new Promise((resolve) => {
|
|
32
|
-
server.server.close(() => {
|
|
33
|
-
resolve()
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
describe('GET /req/:subId', () => {
|
|
40
|
-
it('should query kind 1 events (posts) - covers examples 02, 04', async () => {
|
|
41
|
-
// JB55's public key from example 02
|
|
42
|
-
const jb55 = '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
|
|
43
|
-
|
|
44
|
-
// Create subscription ID
|
|
45
|
-
const subId = 'read-posts-' + Date.now()
|
|
46
|
-
|
|
47
|
-
// Create filters - read posts from JB55
|
|
48
|
-
const filters = {
|
|
49
|
-
limit: 2,
|
|
50
|
-
kinds: [1],
|
|
51
|
-
authors: [jb55]
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Query events using GET /req/:subId
|
|
55
|
-
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
|
56
|
-
const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
|
|
57
|
-
|
|
58
|
-
const response = await fetch(url)
|
|
59
|
-
const events = await response.json()
|
|
60
|
-
|
|
61
|
-
// Assert response
|
|
62
|
-
assert.equal(response.status, 200)
|
|
63
|
-
assert.isArray(events)
|
|
64
|
-
// May be empty if no events exist, but structure should be correct
|
|
65
|
-
if (events.length > 0) {
|
|
66
|
-
assert.property(events[0], 'id')
|
|
67
|
-
assert.property(events[0], 'pubkey')
|
|
68
|
-
assert.property(events[0], 'created_at')
|
|
69
|
-
assert.property(events[0], 'kind')
|
|
70
|
-
assert.property(events[0], 'content')
|
|
71
|
-
assert.equal(events[0].kind, 1)
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('should query Alice posts - covers example 04', async () => {
|
|
76
|
-
// Alice's public key
|
|
77
|
-
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
|
78
|
-
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
|
79
|
-
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
|
80
|
-
|
|
81
|
-
// Create subscription ID
|
|
82
|
-
const subId = 'read-alice-posts-' + Date.now()
|
|
83
|
-
|
|
84
|
-
// Create filters - read posts from Alice
|
|
85
|
-
const filters = {
|
|
86
|
-
limit: 2,
|
|
87
|
-
kinds: [1],
|
|
88
|
-
authors: [alicePubKey]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Query events using GET /req/:subId
|
|
92
|
-
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
|
93
|
-
const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
|
|
94
|
-
|
|
95
|
-
const response = await fetch(url)
|
|
96
|
-
const events = await response.json()
|
|
97
|
-
|
|
98
|
-
// Assert response
|
|
99
|
-
assert.equal(response.status, 200)
|
|
100
|
-
assert.isArray(events)
|
|
101
|
-
if (events.length > 0) {
|
|
102
|
-
assert.equal(events[0].pubkey, alicePubKey)
|
|
103
|
-
assert.equal(events[0].kind, 1)
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('should query kind 3 events (follow list) - covers example 05', async () => {
|
|
108
|
-
// Alice's public key
|
|
109
|
-
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
|
110
|
-
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
|
111
|
-
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
|
112
|
-
|
|
113
|
-
// Create subscription ID
|
|
114
|
-
const subId = 'get-follow-list-' + Date.now()
|
|
115
|
-
|
|
116
|
-
// Create filters - get follow list (kind 3) from Alice
|
|
117
|
-
const filters = {
|
|
118
|
-
limit: 5,
|
|
119
|
-
kinds: [3],
|
|
120
|
-
authors: [alicePubKey]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Query events using GET /req/:subId
|
|
124
|
-
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
|
125
|
-
const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
|
|
126
|
-
|
|
127
|
-
const response = await fetch(url)
|
|
128
|
-
const events = await response.json()
|
|
129
|
-
|
|
130
|
-
// Assert response
|
|
131
|
-
assert.equal(response.status, 200)
|
|
132
|
-
assert.isArray(events)
|
|
133
|
-
if (events.length > 0) {
|
|
134
|
-
assert.equal(events[0].kind, 3)
|
|
135
|
-
assert.equal(events[0].pubkey, alicePubKey)
|
|
136
|
-
assert.isArray(events[0].tags)
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('should handle filters as individual query params', async () => {
|
|
141
|
-
const subId = 'test-sub-' + Date.now()
|
|
142
|
-
const url = `${baseUrl}/req/${subId}?kinds=[1]&limit=10`
|
|
143
|
-
|
|
144
|
-
const response = await fetch(url)
|
|
145
|
-
const events = await response.json()
|
|
146
|
-
|
|
147
|
-
assert.equal(response.status, 200)
|
|
148
|
-
assert.isArray(events)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('should return 400 when subscription ID is missing', async () => {
|
|
152
|
-
const url = `${baseUrl}/req/?filters=${encodeURIComponent(JSON.stringify([{ kinds: [1] }]))}`
|
|
153
|
-
|
|
154
|
-
const response = await fetch(url)
|
|
155
|
-
await response.json()
|
|
156
|
-
|
|
157
|
-
// Should return 404 or 400
|
|
158
|
-
assert.isAtLeast(response.status, 400)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('should return 400 when filters JSON is invalid', async () => {
|
|
162
|
-
const subId = 'test-sub-' + Date.now()
|
|
163
|
-
const url = `${baseUrl}/req/${subId}?filters=invalid-json{`
|
|
164
|
-
|
|
165
|
-
const response = await fetch(url)
|
|
166
|
-
const result = await response.json()
|
|
167
|
-
|
|
168
|
-
assert.equal(response.status, 400)
|
|
169
|
-
assert.property(result, 'error')
|
|
170
|
-
assert.include(result.error, 'Invalid filters JSON')
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
})
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Integration tests for POST /req/:subId SSE subscription and DELETE /req/:subId.
|
|
3
|
-
These tests require a running Nostr relay.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// npm libraries
|
|
7
|
-
import { assert } from 'chai'
|
|
8
|
-
|
|
9
|
-
// Unit under test
|
|
10
|
-
import Server from '../../../bin/server.js'
|
|
11
|
-
|
|
12
|
-
describe('#subscription-integration.js', () => {
|
|
13
|
-
let server
|
|
14
|
-
const baseUrl = 'http://localhost:3003' // Use different port for tests
|
|
15
|
-
|
|
16
|
-
before(async () => {
|
|
17
|
-
// Start test server
|
|
18
|
-
server = new Server()
|
|
19
|
-
server.config.port = 3003
|
|
20
|
-
await server.startServer()
|
|
21
|
-
|
|
22
|
-
// Wait for server to be ready
|
|
23
|
-
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
after(async function () {
|
|
27
|
-
this.timeout(10000) // Increase timeout for cleanup
|
|
28
|
-
// Stop server
|
|
29
|
-
if (server && server.server) {
|
|
30
|
-
// Close all connections forcefully if available
|
|
31
|
-
if (server.server.closeAllConnections) {
|
|
32
|
-
server.server.closeAllConnections()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
await new Promise((resolve) => {
|
|
36
|
-
const timeout = setTimeout(() => {
|
|
37
|
-
resolve() // Force resolve after 2 seconds
|
|
38
|
-
}, 2000)
|
|
39
|
-
|
|
40
|
-
server.server.close(() => {
|
|
41
|
-
clearTimeout(timeout)
|
|
42
|
-
resolve()
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('POST /req/:subId', () => {
|
|
49
|
-
it('should create SSE subscription', async () => {
|
|
50
|
-
const subId = 'test-sub-' + Date.now()
|
|
51
|
-
const filters = { kinds: [1], limit: 10 }
|
|
52
|
-
|
|
53
|
-
const response = await fetch(`${baseUrl}/req/${subId}`, {
|
|
54
|
-
method: 'POST',
|
|
55
|
-
headers: {
|
|
56
|
-
'Content-Type': 'application/json'
|
|
57
|
-
},
|
|
58
|
-
body: JSON.stringify(filters)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
// Assert SSE headers
|
|
62
|
-
assert.equal(response.headers.get('content-type'), 'text/event-stream')
|
|
63
|
-
assert.equal(response.headers.get('cache-control'), 'no-cache')
|
|
64
|
-
assert.equal(response.headers.get('connection'), 'keep-alive')
|
|
65
|
-
|
|
66
|
-
// Read initial connection message
|
|
67
|
-
const reader = response.body.getReader()
|
|
68
|
-
const decoder = new TextDecoder()
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const { value } = await reader.read()
|
|
72
|
-
const text = decoder.decode(value)
|
|
73
|
-
assert.include(text, 'connected')
|
|
74
|
-
assert.include(text, subId)
|
|
75
|
-
} finally {
|
|
76
|
-
reader.releaseLock()
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('should return 400 when subscription ID is missing', async () => {
|
|
81
|
-
const filters = { kinds: [1] }
|
|
82
|
-
|
|
83
|
-
const response = await fetch(`${baseUrl}/req/`, {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
headers: {
|
|
86
|
-
'Content-Type': 'application/json'
|
|
87
|
-
},
|
|
88
|
-
body: JSON.stringify(filters)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
// Should return 404 or 400
|
|
92
|
-
assert.isAtLeast(response.status, 400)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('should return 400 when filters are missing', async () => {
|
|
96
|
-
const subId = 'test-sub-' + Date.now()
|
|
97
|
-
|
|
98
|
-
const response = await fetch(`${baseUrl}/req/${subId}`, {
|
|
99
|
-
method: 'POST',
|
|
100
|
-
headers: {
|
|
101
|
-
'Content-Type': 'application/json'
|
|
102
|
-
},
|
|
103
|
-
body: JSON.stringify({})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
const result = await response.json()
|
|
107
|
-
|
|
108
|
-
assert.equal(response.status, 400)
|
|
109
|
-
assert.property(result, 'error')
|
|
110
|
-
assert.include(result.error, 'Filters are required')
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
describe('DELETE /req/:subId', () => {
|
|
115
|
-
it('should close a subscription', async () => {
|
|
116
|
-
const subId = 'test-sub-' + Date.now()
|
|
117
|
-
|
|
118
|
-
const response = await fetch(`${baseUrl}/req/${subId}`, {
|
|
119
|
-
method: 'DELETE'
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
// May return 200 if subscription exists, or 500 if it doesn't
|
|
123
|
-
// The important thing is it doesn't crash
|
|
124
|
-
assert.isAtMost(response.status, 500)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
it('should return 400 when subscription ID is missing', async () => {
|
|
128
|
-
const response = await fetch(`${baseUrl}/req/`, {
|
|
129
|
-
method: 'DELETE'
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
// Should return 404 or 400
|
|
133
|
-
assert.isAtLeast(response.status, 400)
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
describe('PUT /req/:subId', () => {
|
|
138
|
-
it('should create SSE subscription (alternative method)', async function () {
|
|
139
|
-
this.timeout(10000) // Increase timeout for this test
|
|
140
|
-
|
|
141
|
-
const subId = 'test-sub-' + Date.now()
|
|
142
|
-
const filters = { kinds: [1], limit: 10 }
|
|
143
|
-
|
|
144
|
-
const controller = new AbortController()
|
|
145
|
-
const timeoutId = setTimeout(() => controller.abort(), 8000)
|
|
146
|
-
|
|
147
|
-
let response
|
|
148
|
-
try {
|
|
149
|
-
response = await fetch(`${baseUrl}/req/${subId}`, {
|
|
150
|
-
method: 'PUT',
|
|
151
|
-
headers: {
|
|
152
|
-
'Content-Type': 'application/json'
|
|
153
|
-
},
|
|
154
|
-
body: JSON.stringify(filters),
|
|
155
|
-
signal: controller.signal
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
// Assert SSE headers
|
|
159
|
-
assert.equal(response.headers.get('content-type'), 'text/event-stream')
|
|
160
|
-
|
|
161
|
-
clearTimeout(timeoutId)
|
|
162
|
-
|
|
163
|
-
// Consume the stream to prevent hanging
|
|
164
|
-
const reader = response.body.getReader()
|
|
165
|
-
const decoder = new TextDecoder()
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
// Read initial connection message with timeout
|
|
169
|
-
const readPromise = reader.read()
|
|
170
|
-
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve({ value: null, done: true }), 2000))
|
|
171
|
-
const { value, done } = await Promise.race([readPromise, timeoutPromise])
|
|
172
|
-
|
|
173
|
-
if (value && !done) {
|
|
174
|
-
const text = decoder.decode(value)
|
|
175
|
-
assert.include(text, 'connected')
|
|
176
|
-
}
|
|
177
|
-
} finally {
|
|
178
|
-
reader.releaseLock()
|
|
179
|
-
}
|
|
180
|
-
} catch (err) {
|
|
181
|
-
// AbortController aborted - this is expected
|
|
182
|
-
if (err.name !== 'AbortError') {
|
|
183
|
-
throw err
|
|
184
|
-
}
|
|
185
|
-
} finally {
|
|
186
|
-
clearTimeout(timeoutId)
|
|
187
|
-
// Always close the subscription
|
|
188
|
-
try {
|
|
189
|
-
await fetch(`${baseUrl}/req/${subId}`, {
|
|
190
|
-
method: 'DELETE'
|
|
191
|
-
})
|
|
192
|
-
} catch (err) {
|
|
193
|
-
// Ignore errors when closing
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
})
|
|
198
|
-
})
|