mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api.git
synced 2026-09-21 16:52:00 -07:00
first commit
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
Integration tests for POST /event endpoint.
|
||||
These tests require a running Nostr relay.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
// Unit under test
|
||||
import Server from '../../../bin/server.js'
|
||||
import { finalizeEvent, getPublicKey, generateSecretKey } from 'nostr-tools/pure'
|
||||
import { hexToBytes } from '@noble/hashes/utils.js'
|
||||
|
||||
describe('#event-integration.js', () => {
|
||||
let server
|
||||
const baseUrl = 'http://localhost:3001' // Use different port for tests
|
||||
|
||||
before(async () => {
|
||||
// Start test server
|
||||
server = new Server()
|
||||
server.config.port = 3001
|
||||
await server.startServer()
|
||||
|
||||
// Wait for server to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Stop server
|
||||
if (server && server.server) {
|
||||
await new Promise((resolve) => {
|
||||
server.server.close(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('POST /event', () => {
|
||||
it('should publish kind 0 event (profile metadata) - covers example 01', async () => {
|
||||
// Generate keys
|
||||
const sk = generateSecretKey()
|
||||
|
||||
// Create profile metadata event (kind 0)
|
||||
const profileMetadata = {
|
||||
name: 'Test User',
|
||||
about: 'Integration test user',
|
||||
picture: 'https://example.com/test.jpg'
|
||||
}
|
||||
|
||||
const eventTemplate = {
|
||||
kind: 0,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: JSON.stringify(profileMetadata)
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = finalizeEvent(eventTemplate, sk)
|
||||
|
||||
// Publish to REST API
|
||||
const response = await fetch(`${baseUrl}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signedEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Assert response
|
||||
assert.equal(response.status, 200)
|
||||
assert.property(result, 'accepted')
|
||||
assert.property(result, 'eventId')
|
||||
assert.equal(result.eventId, signedEvent.id)
|
||||
})
|
||||
|
||||
it('should publish kind 1 event (text post) - covers example 03', async () => {
|
||||
// Alice's private key from examples
|
||||
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
||||
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
||||
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
||||
|
||||
// Generate a post
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'Integration test post'
|
||||
}
|
||||
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, alicePrivKeyBin)
|
||||
|
||||
// Publish to REST API
|
||||
const response = await fetch(`${baseUrl}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signedEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Assert response
|
||||
assert.equal(response.status, 200)
|
||||
assert.property(result, 'accepted')
|
||||
assert.property(result, 'eventId')
|
||||
assert.equal(result.eventId, signedEvent.id)
|
||||
assert.equal(signedEvent.pubkey, alicePubKey)
|
||||
})
|
||||
|
||||
it('should publish kind 3 event (follow list) - covers example 06', async () => {
|
||||
// Alice's private key
|
||||
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
||||
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
||||
|
||||
// Bob's public key
|
||||
const bobPrivKeyHex = 'd2e71a977bc3900d6b0f787421e3d1a666cd12ca625482b0d9eeffd23489c99f'
|
||||
const bobPrivKeyBin = hexToBytes(bobPrivKeyHex)
|
||||
const bobPubKey = getPublicKey(bobPrivKeyBin)
|
||||
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
const followList = [
|
||||
['p', bobPubKey, psf, 'bob']
|
||||
]
|
||||
|
||||
// Generate a follow list event (kind 3)
|
||||
const eventTemplate = {
|
||||
kind: 3,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: followList,
|
||||
content: ''
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = finalizeEvent(eventTemplate, alicePrivKeyBin)
|
||||
|
||||
// Publish to REST API
|
||||
const response = await fetch(`${baseUrl}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signedEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Assert response
|
||||
assert.equal(response.status, 200)
|
||||
assert.property(result, 'accepted')
|
||||
assert.property(result, 'eventId')
|
||||
assert.equal(result.eventId, signedEvent.id)
|
||||
})
|
||||
|
||||
it('should publish kind 7 event (reaction/like) - covers example 07', async () => {
|
||||
// Bob's private key
|
||||
const bobPrivKeyHex = 'd2e71a977bc3900d6b0f787421e3d1a666cd12ca625482b0d9eeffd23489c99f'
|
||||
const bobPrivKeyBin = hexToBytes(bobPrivKeyHex)
|
||||
const bobPubKey = getPublicKey(bobPrivKeyBin)
|
||||
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
// Use a test event ID
|
||||
const evIdToLike = 'd09b4c5da59be3cd2768aa53fa78b77bf4859084c94f3bf26d401f004a9c8167'
|
||||
const evIdAuthorPubKey = '2c7e76c0f8dc1dca9d0197c7d19be580a8d074ccada6a2f6ebe056ae41092e92'
|
||||
|
||||
// Generate like event (kind 7)
|
||||
const likeEventTemplate = {
|
||||
kind: 7,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
pubkey: bobPubKey,
|
||||
tags: [
|
||||
['e', evIdToLike, psf],
|
||||
['p', evIdAuthorPubKey, psf]
|
||||
],
|
||||
content: '+'
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = finalizeEvent(likeEventTemplate, bobPrivKeyBin)
|
||||
|
||||
// Publish to REST API
|
||||
const response = await fetch(`${baseUrl}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(signedEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Assert response
|
||||
assert.equal(response.status, 200)
|
||||
assert.property(result, 'accepted')
|
||||
assert.property(result, 'eventId')
|
||||
assert.equal(result.eventId, signedEvent.id)
|
||||
})
|
||||
|
||||
it('should reject invalid event', async () => {
|
||||
const invalidEvent = {
|
||||
id: 'invalid',
|
||||
pubkey: 'invalid',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: 'invalid'
|
||||
}
|
||||
|
||||
const response = await fetch(`${baseUrl}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(invalidEvent)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
// Should reject invalid event - error response format
|
||||
assert.equal(response.status, 400)
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Invalid event structure')
|
||||
})
|
||||
|
||||
it('should return 400 when event data is missing', async () => {
|
||||
// Send empty body - Express will parse as undefined, controller should handle it
|
||||
const response = await fetch(`${baseUrl}/event`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: ''
|
||||
})
|
||||
|
||||
// Empty body should be parsed as undefined by Express
|
||||
const result = await response.json()
|
||||
|
||||
assert.equal(response.status, 400)
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Event data is required')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
Integration tests for GET /req/:subId endpoint.
|
||||
These tests require a running Nostr relay.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
// Unit under test
|
||||
import Server from '../../../bin/server.js'
|
||||
import { getPublicKey } from 'nostr-tools/pure'
|
||||
import { hexToBytes } from '@noble/hashes/utils.js'
|
||||
|
||||
describe('#req-integration.js', () => {
|
||||
let server
|
||||
const baseUrl = 'http://localhost:3002' // Use different port for tests
|
||||
|
||||
before(async () => {
|
||||
// Start test server
|
||||
server = new Server()
|
||||
server.config.port = 3002
|
||||
await server.startServer()
|
||||
|
||||
// Wait for server to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Stop server
|
||||
if (server && server.server) {
|
||||
await new Promise((resolve) => {
|
||||
server.server.close(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('GET /req/:subId', () => {
|
||||
it('should query kind 1 events (posts) - covers examples 02, 04', async () => {
|
||||
// JB55's public key from example 02
|
||||
const jb55 = '32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245'
|
||||
|
||||
// Create subscription ID
|
||||
const subId = 'read-posts-' + Date.now()
|
||||
|
||||
// Create filters - read posts from JB55
|
||||
const filters = {
|
||||
limit: 2,
|
||||
kinds: [1],
|
||||
authors: [jb55]
|
||||
}
|
||||
|
||||
// Query events using GET /req/:subId
|
||||
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
||||
const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
|
||||
|
||||
const response = await fetch(url)
|
||||
const events = await response.json()
|
||||
|
||||
// Assert response
|
||||
assert.equal(response.status, 200)
|
||||
assert.isArray(events)
|
||||
// May be empty if no events exist, but structure should be correct
|
||||
if (events.length > 0) {
|
||||
assert.property(events[0], 'id')
|
||||
assert.property(events[0], 'pubkey')
|
||||
assert.property(events[0], 'created_at')
|
||||
assert.property(events[0], 'kind')
|
||||
assert.property(events[0], 'content')
|
||||
assert.equal(events[0].kind, 1)
|
||||
}
|
||||
})
|
||||
|
||||
it('should query Alice posts - covers example 04', async () => {
|
||||
// Alice's public key
|
||||
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
||||
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
||||
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
||||
|
||||
// Create subscription ID
|
||||
const subId = 'read-alice-posts-' + Date.now()
|
||||
|
||||
// Create filters - read posts from Alice
|
||||
const filters = {
|
||||
limit: 2,
|
||||
kinds: [1],
|
||||
authors: [alicePubKey]
|
||||
}
|
||||
|
||||
// Query events using GET /req/:subId
|
||||
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
||||
const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
|
||||
|
||||
const response = await fetch(url)
|
||||
const events = await response.json()
|
||||
|
||||
// Assert response
|
||||
assert.equal(response.status, 200)
|
||||
assert.isArray(events)
|
||||
if (events.length > 0) {
|
||||
assert.equal(events[0].pubkey, alicePubKey)
|
||||
assert.equal(events[0].kind, 1)
|
||||
}
|
||||
})
|
||||
|
||||
it('should query kind 3 events (follow list) - covers example 05', async () => {
|
||||
// Alice's public key
|
||||
const alicePrivKeyHex = '3292a48aa331aeccce003d50d70fbd79617ba91860abbd2c78fa4a8301e36bc0'
|
||||
const alicePrivKeyBin = hexToBytes(alicePrivKeyHex)
|
||||
const alicePubKey = getPublicKey(alicePrivKeyBin)
|
||||
|
||||
// Create subscription ID
|
||||
const subId = 'get-follow-list-' + Date.now()
|
||||
|
||||
// Create filters - get follow list (kind 3) from Alice
|
||||
const filters = {
|
||||
limit: 5,
|
||||
kinds: [3],
|
||||
authors: [alicePubKey]
|
||||
}
|
||||
|
||||
// Query events using GET /req/:subId
|
||||
const filtersJson = encodeURIComponent(JSON.stringify([filters]))
|
||||
const url = `${baseUrl}/req/${subId}?filters=${filtersJson}`
|
||||
|
||||
const response = await fetch(url)
|
||||
const events = await response.json()
|
||||
|
||||
// Assert response
|
||||
assert.equal(response.status, 200)
|
||||
assert.isArray(events)
|
||||
if (events.length > 0) {
|
||||
assert.equal(events[0].kind, 3)
|
||||
assert.equal(events[0].pubkey, alicePubKey)
|
||||
assert.isArray(events[0].tags)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle filters as individual query params', async () => {
|
||||
const subId = 'test-sub-' + Date.now()
|
||||
const url = `${baseUrl}/req/${subId}?kinds=[1]&limit=10`
|
||||
|
||||
const response = await fetch(url)
|
||||
const events = await response.json()
|
||||
|
||||
assert.equal(response.status, 200)
|
||||
assert.isArray(events)
|
||||
})
|
||||
|
||||
it('should return 400 when subscription ID is missing', async () => {
|
||||
const url = `${baseUrl}/req/?filters=${encodeURIComponent(JSON.stringify([{ kinds: [1] }]))}`
|
||||
|
||||
const response = await fetch(url)
|
||||
await response.json()
|
||||
|
||||
// Should return 404 or 400
|
||||
assert.isAtLeast(response.status, 400)
|
||||
})
|
||||
|
||||
it('should return 400 when filters JSON is invalid', async () => {
|
||||
const subId = 'test-sub-' + Date.now()
|
||||
const url = `${baseUrl}/req/${subId}?filters=invalid-json{`
|
||||
|
||||
const response = await fetch(url)
|
||||
const result = await response.json()
|
||||
|
||||
assert.equal(response.status, 400)
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Invalid filters JSON')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
Integration tests for POST /req/:subId SSE subscription and DELETE /req/:subId.
|
||||
These tests require a running Nostr relay.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
// Unit under test
|
||||
import Server from '../../../bin/server.js'
|
||||
|
||||
describe('#subscription-integration.js', () => {
|
||||
let server
|
||||
const baseUrl = 'http://localhost:3003' // Use different port for tests
|
||||
|
||||
before(async () => {
|
||||
// Start test server
|
||||
server = new Server()
|
||||
server.config.port = 3003
|
||||
await server.startServer()
|
||||
|
||||
// Wait for server to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
this.timeout(10000) // Increase timeout for cleanup
|
||||
// Stop server
|
||||
if (server && server.server) {
|
||||
// Close all connections forcefully if available
|
||||
if (server.server.closeAllConnections) {
|
||||
server.server.closeAllConnections()
|
||||
}
|
||||
|
||||
await new Promise((resolve) => {
|
||||
const timeout = setTimeout(() => {
|
||||
resolve() // Force resolve after 2 seconds
|
||||
}, 2000)
|
||||
|
||||
server.server.close(() => {
|
||||
clearTimeout(timeout)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('POST /req/:subId', () => {
|
||||
it('should create SSE subscription', async () => {
|
||||
const subId = 'test-sub-' + Date.now()
|
||||
const filters = { kinds: [1], limit: 10 }
|
||||
|
||||
const response = await fetch(`${baseUrl}/req/${subId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(filters)
|
||||
})
|
||||
|
||||
// Assert SSE headers
|
||||
assert.equal(response.headers.get('content-type'), 'text/event-stream')
|
||||
assert.equal(response.headers.get('cache-control'), 'no-cache')
|
||||
assert.equal(response.headers.get('connection'), 'keep-alive')
|
||||
|
||||
// Read initial connection message
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
try {
|
||||
const { value } = await reader.read()
|
||||
const text = decoder.decode(value)
|
||||
assert.include(text, 'connected')
|
||||
assert.include(text, subId)
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 400 when subscription ID is missing', async () => {
|
||||
const filters = { kinds: [1] }
|
||||
|
||||
const response = await fetch(`${baseUrl}/req/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(filters)
|
||||
})
|
||||
|
||||
// Should return 404 or 400
|
||||
assert.isAtLeast(response.status, 400)
|
||||
})
|
||||
|
||||
it('should return 400 when filters are missing', async () => {
|
||||
const subId = 'test-sub-' + Date.now()
|
||||
|
||||
const response = await fetch(`${baseUrl}/req/${subId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
assert.equal(response.status, 400)
|
||||
assert.property(result, 'error')
|
||||
assert.include(result.error, 'Filters are required')
|
||||
})
|
||||
})
|
||||
|
||||
describe('DELETE /req/:subId', () => {
|
||||
it('should close a subscription', async () => {
|
||||
const subId = 'test-sub-' + Date.now()
|
||||
|
||||
const response = await fetch(`${baseUrl}/req/${subId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
// May return 200 if subscription exists, or 500 if it doesn't
|
||||
// The important thing is it doesn't crash
|
||||
assert.isAtMost(response.status, 500)
|
||||
})
|
||||
|
||||
it('should return 400 when subscription ID is missing', async () => {
|
||||
const response = await fetch(`${baseUrl}/req/`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
// Should return 404 or 400
|
||||
assert.isAtLeast(response.status, 400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('PUT /req/:subId', () => {
|
||||
it('should create SSE subscription (alternative method)', async function () {
|
||||
this.timeout(10000) // Increase timeout for this test
|
||||
|
||||
const subId = 'test-sub-' + Date.now()
|
||||
const filters = { kinds: [1], limit: 10 }
|
||||
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 8000)
|
||||
|
||||
let response
|
||||
try {
|
||||
response = await fetch(`${baseUrl}/req/${subId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(filters),
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
// Assert SSE headers
|
||||
assert.equal(response.headers.get('content-type'), 'text/event-stream')
|
||||
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
// Consume the stream to prevent hanging
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
try {
|
||||
// Read initial connection message with timeout
|
||||
const readPromise = reader.read()
|
||||
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve({ value: null, done: true }), 2000))
|
||||
const { value, done } = await Promise.race([readPromise, timeoutPromise])
|
||||
|
||||
if (value && !done) {
|
||||
const text = decoder.decode(value)
|
||||
assert.include(text, 'connected')
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
} catch (err) {
|
||||
// AbortController aborted - this is expected
|
||||
if (err.name !== 'AbortError') {
|
||||
throw err
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeoutId)
|
||||
// Always close the subscription
|
||||
try {
|
||||
await fetch(`${baseUrl}/req/${subId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
} catch (err) {
|
||||
// Ignore errors when closing
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
Integration tests for ManageSubscriptionUseCase with real adapter.
|
||||
These tests require a running Nostr relay.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
// Unit under test
|
||||
import Adapters from '../../../src/adapters/index.js'
|
||||
import ManageSubscriptionUseCase from '../../../src/use-cases/manage-subscription.js'
|
||||
|
||||
describe('#manage-subscription-integration.js', () => {
|
||||
let adapters
|
||||
let uut
|
||||
|
||||
before(async () => {
|
||||
// Initialize adapters (will connect to real relay)
|
||||
adapters = new Adapters()
|
||||
await adapters.start()
|
||||
|
||||
uut = new ManageSubscriptionUseCase({ adapters })
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Clean up all subscriptions and disconnect from all relays
|
||||
// Note: This is a simplified cleanup - in production you'd track all subscriptions
|
||||
if (adapters && adapters.nostrRelays) {
|
||||
await Promise.allSettled(
|
||||
adapters.nostrRelays.map(relay => relay.disconnect())
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('#createSubscription()', () => {
|
||||
it('should successfully create a subscription', async () => {
|
||||
const subscriptionId = 'test-sub-' + Date.now()
|
||||
const filters = [{ kinds: [1], limit: 5 }]
|
||||
|
||||
let eventReceived = false
|
||||
let eoseReceived = false
|
||||
|
||||
const onEvent = (event) => {
|
||||
eventReceived = true
|
||||
assert.property(event, 'id')
|
||||
assert.property(event, 'kind')
|
||||
}
|
||||
|
||||
const onEose = () => {
|
||||
eoseReceived = true
|
||||
}
|
||||
|
||||
const onClosed = () => {
|
||||
// Handler for closed events
|
||||
}
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters, onEvent, onEose, onClosed)
|
||||
|
||||
// Assert subscription exists
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
|
||||
// Wait a bit for events/EOSE
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// EOSE should be received (or events)
|
||||
// Note: May not receive events if none exist, but EOSE should come
|
||||
assert.isTrue(eoseReceived || eventReceived)
|
||||
|
||||
// Clean up
|
||||
if (uut.hasSubscription(subscriptionId)) {
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
}
|
||||
})
|
||||
|
||||
it('should prevent duplicate subscriptions', async () => {
|
||||
const subscriptionId = 'test-dup-' + Date.now()
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
|
||||
try {
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'already exists')
|
||||
}
|
||||
|
||||
// Clean up
|
||||
if (uut.hasSubscription(subscriptionId)) {
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle subscription with no events', async () => {
|
||||
const subscriptionId = 'test-empty-' + Date.now()
|
||||
const filters = [{ kinds: [99999], limit: 1 }] // Unlikely to have events
|
||||
|
||||
let eoseReceived = false
|
||||
|
||||
const onEose = () => {
|
||||
eoseReceived = true
|
||||
}
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters, null, onEose, null)
|
||||
|
||||
// Wait for EOSE
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// Should receive EOSE even with no events
|
||||
assert.isTrue(eoseReceived)
|
||||
|
||||
// Clean up
|
||||
if (uut.hasSubscription(subscriptionId)) {
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('#closeSubscription()', () => {
|
||||
it('should successfully close a subscription', async () => {
|
||||
const subscriptionId = 'test-close-' + Date.now()
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
|
||||
// Assert subscription is removed
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
|
||||
it('should return successfully when closing non-existent subscription (idempotent)', async () => {
|
||||
const subscriptionId = 'non-existent-sub'
|
||||
|
||||
// Should not throw - idempotent operation
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
|
||||
// Should return successfully without error
|
||||
assert.isTrue(true, 'closeSubscription should succeed for non-existent subscription')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hasSubscription()', () => {
|
||||
it('should return false for non-existent subscription', () => {
|
||||
assert.isFalse(uut.hasSubscription('non-existent'))
|
||||
})
|
||||
|
||||
it('should return true for existing subscription', async () => {
|
||||
const subscriptionId = 'test-has-' + Date.now()
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
|
||||
// Clean up
|
||||
if (uut.hasSubscription(subscriptionId)) {
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
Integration tests for PublishEventUseCase with real adapter.
|
||||
These tests require a running Nostr relay.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
// Unit under test
|
||||
import Adapters from '../../../src/adapters/index.js'
|
||||
import PublishEventUseCase from '../../../src/use-cases/publish-event.js'
|
||||
import { finalizeEvent, generateSecretKey } from 'nostr-tools/pure'
|
||||
|
||||
describe('#publish-event-integration.js', () => {
|
||||
let adapters
|
||||
let uut
|
||||
|
||||
before(async () => {
|
||||
// Initialize adapters (will connect to real relay)
|
||||
adapters = new Adapters()
|
||||
await adapters.start()
|
||||
|
||||
uut = new PublishEventUseCase({ adapters })
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Clean up adapters - disconnect from all relays
|
||||
if (adapters && adapters.nostrRelays) {
|
||||
await Promise.allSettled(
|
||||
adapters.nostrRelays.map(relay => relay.disconnect())
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('#execute()', () => {
|
||||
it('should successfully publish a valid event', async () => {
|
||||
// Generate keys
|
||||
const sk = generateSecretKey()
|
||||
|
||||
// Create event template
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'Integration test post from use case'
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const signedEvent = finalizeEvent(eventTemplate, sk)
|
||||
|
||||
// Execute use case
|
||||
const result = await uut.execute(signedEvent)
|
||||
|
||||
// Assert result
|
||||
assert.property(result, 'accepted')
|
||||
assert.property(result, 'message')
|
||||
assert.property(result, 'eventId')
|
||||
assert.equal(result.eventId, signedEvent.id)
|
||||
})
|
||||
|
||||
it('should reject invalid event structure', async () => {
|
||||
const invalidEvent = {
|
||||
id: 'invalid',
|
||||
pubkey: 'invalid',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: 'invalid'
|
||||
}
|
||||
|
||||
try {
|
||||
await uut.execute(invalidEvent)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Invalid event structure')
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle relay rejection', async () => {
|
||||
// Generate keys
|
||||
const sk = generateSecretKey()
|
||||
|
||||
// Create a duplicate event (if we send same event twice)
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'Duplicate test post'
|
||||
}
|
||||
|
||||
const signedEvent = finalizeEvent(eventTemplate, sk)
|
||||
|
||||
// Publish first time
|
||||
const result1 = await uut.execute(signedEvent)
|
||||
assert.property(result1, 'accepted')
|
||||
|
||||
// Try to publish again (may be rejected as duplicate)
|
||||
const result2 = await uut.execute(signedEvent)
|
||||
assert.property(result2, 'accepted')
|
||||
// Result may be accepted or rejected depending on relay
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
Integration tests for QueryEventsUseCase with real adapter.
|
||||
These tests require a running Nostr relay.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
// Unit under test
|
||||
import Adapters from '../../../src/adapters/index.js'
|
||||
import QueryEventsUseCase from '../../../src/use-cases/query-events.js'
|
||||
|
||||
describe('#query-events-integration.js', () => {
|
||||
let adapters
|
||||
let uut
|
||||
|
||||
before(async () => {
|
||||
// Initialize adapters (will connect to real relay)
|
||||
adapters = new Adapters()
|
||||
await adapters.start()
|
||||
|
||||
uut = new QueryEventsUseCase({ adapters })
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Clean up adapters - disconnect from all relays
|
||||
if (adapters && adapters.nostrRelays) {
|
||||
await Promise.allSettled(
|
||||
adapters.nostrRelays.map(relay => relay.disconnect())
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('#execute()', () => {
|
||||
it('should successfully query events', async () => {
|
||||
const filters = [{ kinds: [1], limit: 5 }]
|
||||
const subscriptionId = 'test-query-' + Date.now()
|
||||
|
||||
const events = await uut.execute(filters, subscriptionId)
|
||||
|
||||
// Assert result is an array
|
||||
assert.isArray(events)
|
||||
|
||||
// If events are returned, verify structure
|
||||
if (events.length > 0) {
|
||||
assert.property(events[0], 'id')
|
||||
assert.property(events[0], 'pubkey')
|
||||
assert.property(events[0], 'created_at')
|
||||
assert.property(events[0], 'kind')
|
||||
assert.property(events[0], 'content')
|
||||
assert.equal(events[0].kind, 1)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle empty results', async () => {
|
||||
// Query for events that likely don't exist
|
||||
const filters = [{ kinds: [99999], limit: 1 }]
|
||||
const subscriptionId = 'test-empty-' + Date.now()
|
||||
|
||||
const events = await uut.execute(filters, subscriptionId)
|
||||
|
||||
// Should return empty array, not throw
|
||||
assert.isArray(events)
|
||||
assert.equal(events.length, 0)
|
||||
})
|
||||
|
||||
it('should handle multiple filters', async function () {
|
||||
// Increase timeout for this test - needs to be longer than use case timeout (30s)
|
||||
this.timeout(35000)
|
||||
|
||||
const filters = [
|
||||
{ kinds: [1], limit: 2 },
|
||||
{ kinds: [3], limit: 2 }
|
||||
]
|
||||
const subscriptionId = 'test-multi-' + Date.now()
|
||||
|
||||
const events = await uut.execute(filters, subscriptionId)
|
||||
|
||||
// Should return array (may be empty)
|
||||
assert.isArray(events)
|
||||
})
|
||||
|
||||
it('should timeout if EOSE not received', async () => {
|
||||
// This test may take up to 30 seconds
|
||||
// Use a filter that might not return EOSE quickly
|
||||
const filters = [{ kinds: [1] }] // No limit, might timeout
|
||||
const subscriptionId = 'test-timeout-' + Date.now()
|
||||
|
||||
// Should eventually return (even if empty)
|
||||
const events = await uut.execute(filters, subscriptionId)
|
||||
|
||||
assert.isArray(events)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
Unit tests for NostrRelayAdapter.
|
||||
*/
|
||||
|
||||
/*
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import {
|
||||
mockKind1Event,
|
||||
validEventId
|
||||
} from '../mocks/event-mocks.js'
|
||||
import {
|
||||
mockOkAccepted,
|
||||
mockEventMessage,
|
||||
mockEoseMessage,
|
||||
mockClosedMessage
|
||||
} from '../mocks/nostr-relay-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
// Note: WebSocket mocking for ES modules is complex. These tests focus on
|
||||
// testing the adapter's logic that can be tested without full WebSocket mocking.
|
||||
import NostrRelayAdapter from '../../../src/adapters/nostr-relay.js'
|
||||
|
||||
describe('#nostr-relay.js', () => {
|
||||
let sandbox
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
uut = new NostrRelayAdapter({
|
||||
relayUrl: 'wss://test-relay.example.com'
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#connect()', () => {
|
||||
it('should return immediately if already connected', async () => {
|
||||
// Manually set connection state
|
||||
uut.isConnected = true
|
||||
uut.ws = { close: sandbox.stub() }
|
||||
|
||||
await uut.connect()
|
||||
|
||||
// Should not create new connection
|
||||
assert.isTrue(uut.isConnected)
|
||||
})
|
||||
|
||||
// Note: Full WebSocket connection testing requires integration tests
|
||||
// due to ES module import limitations
|
||||
})
|
||||
|
||||
describe('#sendEvent()', () => {
|
||||
it('should queue message when disconnected', async () => {
|
||||
uut.isConnected = false
|
||||
uut.ws = null
|
||||
|
||||
// Mock connect to resolve immediately
|
||||
uut.connect = sandbox.stub().resolves()
|
||||
|
||||
// Start sending (will queue)
|
||||
uut.sendEvent(mockKind1Event).catch(() => {
|
||||
// Expected to fail or timeout without real WebSocket
|
||||
})
|
||||
|
||||
// Should queue message and attempt connection
|
||||
// Wait a bit for async operations
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
assert.isTrue(uut.pendingMessages.length > 0 || uut.connect.called)
|
||||
})
|
||||
|
||||
it('should set up event resolver', async () => {
|
||||
uut.isConnected = true
|
||||
uut.ws = { send: sandbox.stub() }
|
||||
// Mock sendMessage to resolve immediately
|
||||
uut.sendMessage = sandbox.stub().resolves()
|
||||
|
||||
// Start sending
|
||||
const sendPromise = uut.sendEvent(mockKind1Event).catch(() => {
|
||||
// Expected without real WebSocket response
|
||||
})
|
||||
|
||||
// Wait a tick for Promise constructor to run
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
|
||||
// Verify resolver was set up
|
||||
assert.isTrue(uut.eventResolvers.has(mockKind1Event.id))
|
||||
|
||||
// Clean up
|
||||
uut.eventResolvers.delete(mockKind1Event.id)
|
||||
// Prevent timeout error
|
||||
sendPromise.catch(() => {})
|
||||
})
|
||||
|
||||
// Note: Full sendEvent testing with WebSocket responses requires integration tests
|
||||
})
|
||||
|
||||
describe('#sendReq()', () => {
|
||||
it('should store handlers for subscription', async () => {
|
||||
uut.isConnected = true
|
||||
uut.ws = { send: sandbox.stub() }
|
||||
uut.connect = sandbox.stub().resolves()
|
||||
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
const handlers = {
|
||||
onEvent: sandbox.stub(),
|
||||
onEose: sandbox.stub(),
|
||||
onClosed: sandbox.stub()
|
||||
}
|
||||
|
||||
await uut.sendReq(subscriptionId, filters, handlers)
|
||||
|
||||
// Assert handlers were stored
|
||||
assert.isTrue(uut.subscriptionHandlers.has(subscriptionId))
|
||||
assert.deepEqual(uut.subscriptionHandlers.get(subscriptionId), handlers)
|
||||
})
|
||||
|
||||
it('should connect before sending if disconnected', async () => {
|
||||
uut.isConnected = false
|
||||
uut.connect = sandbox.stub().resolves()
|
||||
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
const handlers = {}
|
||||
|
||||
await uut.sendReq(subscriptionId, filters, handlers)
|
||||
|
||||
assert.isTrue(uut.connect.called)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#sendClose()', () => {
|
||||
it('should clean up handlers for subscription', async () => {
|
||||
uut.isConnected = true
|
||||
uut.ws = { send: sandbox.stub() }
|
||||
|
||||
const subscriptionId = 'test-sub-123'
|
||||
uut.subscriptionHandlers.set(subscriptionId, {})
|
||||
uut.messageHandlers.set(subscriptionId, {})
|
||||
|
||||
await uut.sendClose(subscriptionId)
|
||||
|
||||
// Assert handlers were cleaned up
|
||||
assert.isFalse(uut.subscriptionHandlers.has(subscriptionId))
|
||||
assert.isFalse(uut.messageHandlers.has(subscriptionId))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleMessage()', () => {
|
||||
it('should handle EVENT message', () => {
|
||||
// Use the subscription ID from the mock message
|
||||
const subscriptionId = 'subscription-id-123'
|
||||
const onEventHandler = sandbox.stub()
|
||||
uut.subscriptionHandlers.set(subscriptionId, {
|
||||
onEvent: onEventHandler
|
||||
})
|
||||
|
||||
const message = mockEventMessage
|
||||
uut.handleMessage(message)
|
||||
|
||||
assert.isTrue(onEventHandler.calledOnce)
|
||||
assert.deepEqual(onEventHandler.getCall(0).args[0], mockKind1Event)
|
||||
})
|
||||
|
||||
it('should handle EOSE message', () => {
|
||||
// Use the subscription ID from the mock message
|
||||
const subscriptionId = 'subscription-id-123'
|
||||
const onEoseHandler = sandbox.stub()
|
||||
uut.subscriptionHandlers.set(subscriptionId, {
|
||||
onEose: onEoseHandler
|
||||
})
|
||||
|
||||
const message = mockEoseMessage
|
||||
uut.handleMessage(message)
|
||||
|
||||
assert.isTrue(onEoseHandler.calledOnce)
|
||||
})
|
||||
|
||||
it('should handle CLOSED message', () => {
|
||||
// Use the subscription ID from the mock message
|
||||
const subscriptionId = 'subscription-id-123'
|
||||
const onClosedHandler = sandbox.stub()
|
||||
uut.subscriptionHandlers.set(subscriptionId, {
|
||||
onClosed: onClosedHandler
|
||||
})
|
||||
|
||||
const message = mockClosedMessage
|
||||
uut.handleMessage(message)
|
||||
|
||||
assert.isTrue(onClosedHandler.calledOnce)
|
||||
assert.equal(onClosedHandler.getCall(0).args[0], 'subscription closed')
|
||||
})
|
||||
|
||||
it('should handle OK message', () => {
|
||||
const eventId = validEventId
|
||||
let resolver = null
|
||||
uut.eventResolvers.set(eventId, (result) => {
|
||||
resolver = result
|
||||
})
|
||||
|
||||
const message = mockOkAccepted
|
||||
uut.handleMessage(message)
|
||||
|
||||
assert.isNotNull(resolver)
|
||||
assert.isTrue(resolver.accepted)
|
||||
assert.isFalse(uut.eventResolvers.has(eventId))
|
||||
})
|
||||
|
||||
it('should handle NOTICE message', () => {
|
||||
const message = ['NOTICE', 'rate limited']
|
||||
// Should not throw
|
||||
uut.handleMessage(message)
|
||||
})
|
||||
|
||||
it('should ignore invalid message format', () => {
|
||||
const message = 'invalid'
|
||||
// Should not throw
|
||||
uut.handleMessage(message)
|
||||
})
|
||||
|
||||
it('should ignore empty messages', () => {
|
||||
const message = []
|
||||
// Should not throw
|
||||
uut.handleMessage(message)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#disconnect()', () => {
|
||||
it('should disconnect from relay', async () => {
|
||||
const mockWs = { close: sandbox.stub() }
|
||||
uut.isConnected = true
|
||||
uut.ws = mockWs
|
||||
|
||||
await uut.disconnect()
|
||||
|
||||
assert.isTrue(mockWs.close.called)
|
||||
assert.isFalse(uut.isConnected)
|
||||
assert.isNull(uut.ws)
|
||||
})
|
||||
|
||||
it('should handle disconnect when already disconnected', async () => {
|
||||
uut.isConnected = false
|
||||
uut.ws = null
|
||||
|
||||
await uut.disconnect()
|
||||
|
||||
assert.isFalse(uut.isConnected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleError()', () => {
|
||||
it('should handle WebSocket errors', () => {
|
||||
uut.isConnected = true
|
||||
const error = new Error('WebSocket error')
|
||||
|
||||
uut.handleError(error)
|
||||
|
||||
assert.isFalse(uut.isConnected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#handleClose()', () => {
|
||||
it('should attempt reconnection on close', async () => {
|
||||
uut.isConnected = true
|
||||
uut.reconnectAttempts = 0
|
||||
uut.maxReconnectAttempts = 5
|
||||
|
||||
// Mock connect to avoid actual connection
|
||||
uut.connect = sandbox.stub().resolves()
|
||||
|
||||
uut.handleClose()
|
||||
|
||||
// Wait for reconnection attempt
|
||||
await new Promise(resolve => setTimeout(resolve, 110))
|
||||
|
||||
// Should attempt reconnection
|
||||
assert.equal(uut.reconnectAttempts, 1)
|
||||
})
|
||||
|
||||
it('should stop reconnecting after max attempts', async () => {
|
||||
uut.isConnected = true
|
||||
uut.reconnectAttempts = 5
|
||||
uut.maxReconnectAttempts = 5
|
||||
|
||||
uut.connect = sandbox.stub().resolves()
|
||||
|
||||
uut.handleClose()
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 110))
|
||||
|
||||
// Should not increment beyond max
|
||||
assert.equal(uut.reconnectAttempts, 5)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Unit tests for Server class.
|
||||
Note: Full server testing requires integration tests due to ES module limitations.
|
||||
These tests focus on testable logic.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Unit under test
|
||||
import Server from '../../../bin/server.js'
|
||||
|
||||
describe('#server.js', () => {
|
||||
let sandbox
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
uut = new Server()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#startServer()', () => {
|
||||
// Note: Full server startup testing requires integration tests
|
||||
// due to ES module import limitations with Express and Controllers
|
||||
it('should have startServer method', () => {
|
||||
assert.isFunction(uut.startServer)
|
||||
})
|
||||
|
||||
it('should have controllers property', () => {
|
||||
assert.property(uut, 'controllers')
|
||||
})
|
||||
|
||||
it('should have config property', () => {
|
||||
assert.property(uut, 'config')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#sleep()', () => {
|
||||
it('should sleep for specified milliseconds', async () => {
|
||||
const start = Date.now()
|
||||
await uut.sleep(50)
|
||||
const end = Date.now()
|
||||
|
||||
// Should have slept at least 50ms (allowing some margin)
|
||||
assert.isAtLeast(end - start, 40)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should initialize with controllers and config', () => {
|
||||
const server = new Server()
|
||||
|
||||
assert.property(server, 'controllers')
|
||||
assert.property(server, 'config')
|
||||
assert.property(server, 'process')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
Unit tests for EventRESTControllerLib.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import {
|
||||
mockKind1Event,
|
||||
mockKind0Event
|
||||
} from '../mocks/event-mocks.js'
|
||||
import {
|
||||
createMockRequestWithBody,
|
||||
createMockResponse
|
||||
} from '../mocks/controller-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import EventRESTControllerLib from '../../../src/controllers/rest-api/event/controller.js'
|
||||
|
||||
describe('#event-controller.js', () => {
|
||||
let sandbox
|
||||
let mockUseCases
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock use cases
|
||||
mockUseCases = {
|
||||
publishEvent: {
|
||||
execute: sandbox.stub()
|
||||
}
|
||||
}
|
||||
|
||||
uut = new EventRESTControllerLib({
|
||||
adapters: {},
|
||||
useCases: mockUseCases
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#publishEvent()', () => {
|
||||
it('should successfully publish an event', async () => {
|
||||
const req = createMockRequestWithBody(mockKind1Event)
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.publishEvent.execute.resolves({
|
||||
accepted: true,
|
||||
message: 'event saved',
|
||||
eventId: mockKind1Event.id
|
||||
})
|
||||
|
||||
await uut.publishEvent(req, res)
|
||||
|
||||
// Assert use case was called
|
||||
assert.isTrue(mockUseCases.publishEvent.execute.calledOnce)
|
||||
assert.deepEqual(mockUseCases.publishEvent.execute.getCall(0).args[0], mockKind1Event)
|
||||
|
||||
// Assert response
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.property(res.jsonData, 'accepted')
|
||||
assert.isTrue(res.jsonData.accepted)
|
||||
assert.equal(res.jsonData.eventId, mockKind1Event.id)
|
||||
})
|
||||
|
||||
it('should return 400 when event is rejected', async () => {
|
||||
const req = createMockRequestWithBody(mockKind1Event)
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.publishEvent.execute.resolves({
|
||||
accepted: false,
|
||||
message: 'duplicate: event already exists',
|
||||
eventId: mockKind1Event.id
|
||||
})
|
||||
|
||||
await uut.publishEvent(req, res)
|
||||
|
||||
// Assert response status is 400
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.property(res.jsonData, 'accepted')
|
||||
assert.isFalse(res.jsonData.accepted)
|
||||
})
|
||||
|
||||
it('should return 400 when event data is missing', async () => {
|
||||
const req = createMockRequestWithBody(null)
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.publishEvent(req, res)
|
||||
|
||||
// Assert use case was not called
|
||||
assert.isFalse(mockUseCases.publishEvent.execute.called)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Event data is required')
|
||||
})
|
||||
|
||||
it('should handle use case errors', async () => {
|
||||
const req = createMockRequestWithBody(mockKind1Event)
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.publishEvent.execute.rejects(new Error('Network error'))
|
||||
|
||||
await uut.publishEvent(req, res)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 500)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Network error')
|
||||
})
|
||||
|
||||
it('should return 400 for validation errors', async () => {
|
||||
const req = createMockRequestWithBody(mockKind1Event)
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.publishEvent.execute.rejects(new Error('Invalid event structure'))
|
||||
|
||||
await uut.publishEvent(req, res)
|
||||
|
||||
// Assert validation error returns 400
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Invalid event structure')
|
||||
})
|
||||
|
||||
it('should handle errors with missing message', async () => {
|
||||
const req = createMockRequestWithBody(mockKind1Event)
|
||||
const res = createMockResponse()
|
||||
|
||||
const error = new Error()
|
||||
error.message = undefined
|
||||
mockUseCases.publishEvent.execute.rejects(error)
|
||||
|
||||
await uut.publishEvent(req, res)
|
||||
|
||||
// Assert error response with default message
|
||||
assert.equal(res.statusValue, 500)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.equal(res.jsonData.error, 'Internal server error')
|
||||
})
|
||||
|
||||
it('should publish different event kinds', async () => {
|
||||
const req = createMockRequestWithBody(mockKind0Event)
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.publishEvent.execute.resolves({
|
||||
accepted: true,
|
||||
message: 'event saved',
|
||||
eventId: mockKind0Event.id
|
||||
})
|
||||
|
||||
await uut.publishEvent(req, res)
|
||||
|
||||
assert.isTrue(mockUseCases.publishEvent.execute.calledOnce)
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.isTrue(res.jsonData.accepted)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should require adapters instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new EventRESTControllerLib({ useCases: mockUseCases })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Adapters library required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should require useCases instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new EventRESTControllerLib({ adapters: {} })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Use Cases library required')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
Unit tests for ReqRESTControllerLib.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import { mockEventsArray } from '../mocks/nostr-relay-mocks.js'
|
||||
import {
|
||||
createMockRequestWithParams,
|
||||
createMockResponse
|
||||
} from '../mocks/controller-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import ReqRESTControllerLib from '../../../src/controllers/rest-api/req/controller.js'
|
||||
|
||||
describe('#req-controller.js', () => {
|
||||
let sandbox
|
||||
let mockUseCases
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock use cases
|
||||
mockUseCases = {
|
||||
queryEvents: {
|
||||
execute: sandbox.stub()
|
||||
},
|
||||
manageSubscription: {
|
||||
createSubscription: sandbox.stub(),
|
||||
closeSubscription: sandbox.stub()
|
||||
}
|
||||
}
|
||||
|
||||
uut = new ReqRESTControllerLib({
|
||||
adapters: {},
|
||||
useCases: mockUseCases
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#queryEvents()', () => {
|
||||
it('should successfully query events with filters as JSON string', async () => {
|
||||
const filters = [{ kinds: [1], limit: 10 }]
|
||||
const filtersJson = JSON.stringify(filters)
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.query = { filters: filtersJson }
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.queryEvents.execute.resolves(mockEventsArray)
|
||||
|
||||
await uut.queryEvents(req, res)
|
||||
|
||||
// Assert use case was called with parsed filters
|
||||
assert.isTrue(mockUseCases.queryEvents.execute.calledOnce)
|
||||
const executeArgs = mockUseCases.queryEvents.execute.getCall(0).args
|
||||
assert.deepEqual(executeArgs[0], filters)
|
||||
assert.equal(executeArgs[1], 'test-sub-123')
|
||||
|
||||
// Assert response
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.isArray(res.jsonData)
|
||||
assert.equal(res.jsonData.length, mockEventsArray.length)
|
||||
})
|
||||
|
||||
it('should successfully query events with individual query params', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.query = {
|
||||
kinds: JSON.stringify([1]),
|
||||
authors: JSON.stringify(['abc123']),
|
||||
limit: '10'
|
||||
}
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.queryEvents.execute.resolves(mockEventsArray)
|
||||
|
||||
await uut.queryEvents(req, res)
|
||||
|
||||
// Assert use case was called
|
||||
assert.isTrue(mockUseCases.queryEvents.execute.calledOnce)
|
||||
const executeArgs = mockUseCases.queryEvents.execute.getCall(0).args
|
||||
assert.isArray(executeArgs[0])
|
||||
assert.equal(executeArgs[0][0].kinds[0], 1)
|
||||
assert.equal(executeArgs[0][0].authors[0], 'abc123')
|
||||
assert.equal(executeArgs[0][0].limit, 10)
|
||||
})
|
||||
|
||||
it('should handle empty filters', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.query = {}
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.queryEvents.execute.resolves([])
|
||||
|
||||
await uut.queryEvents(req, res)
|
||||
|
||||
// Assert use case was called with empty filters array
|
||||
assert.isTrue(mockUseCases.queryEvents.execute.calledOnce)
|
||||
const executeArgs = mockUseCases.queryEvents.execute.getCall(0).args
|
||||
assert.deepEqual(executeArgs[0], [{}])
|
||||
})
|
||||
|
||||
it('should return 400 when subscription ID is missing', async () => {
|
||||
const req = createMockRequestWithParams({})
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.queryEvents(req, res)
|
||||
|
||||
// Assert use case was not called
|
||||
assert.isFalse(mockUseCases.queryEvents.execute.called)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Subscription ID is required')
|
||||
})
|
||||
|
||||
it('should return 400 when filters JSON is invalid', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.query = { filters: 'invalid-json{' }
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.queryEvents(req, res)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Invalid filters JSON')
|
||||
})
|
||||
|
||||
it('should handle use case errors', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.query = { filters: JSON.stringify([{ kinds: [1] }]) }
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.queryEvents.execute.rejects(new Error('Query failed'))
|
||||
|
||||
await uut.queryEvents(req, res)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 500)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Query failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createSubscription()', () => {
|
||||
it('should successfully create SSE subscription', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.body = { kinds: [1] }
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.manageSubscription.createSubscription.resolves()
|
||||
|
||||
await uut.createSubscription(req, res)
|
||||
|
||||
// Assert use case was called
|
||||
assert.isTrue(mockUseCases.manageSubscription.createSubscription.calledOnce)
|
||||
const createArgs = mockUseCases.manageSubscription.createSubscription.getCall(0).args
|
||||
assert.equal(createArgs[0], 'test-sub-123')
|
||||
assert.isArray(createArgs[1])
|
||||
assert.equal(createArgs[1][0].kinds[0], 1)
|
||||
assert.isFunction(createArgs[2]) // onEvent
|
||||
assert.isFunction(createArgs[3]) // onEose
|
||||
assert.isFunction(createArgs[4]) // onClosed
|
||||
|
||||
// Assert SSE headers
|
||||
assert.equal(res.headers['Content-Type'], 'text/event-stream')
|
||||
assert.equal(res.headers['Cache-Control'], 'no-cache')
|
||||
assert.equal(res.headers.Connection, 'keep-alive')
|
||||
|
||||
// Assert initial connection message was written
|
||||
assert.isTrue(res.writeData.length > 0)
|
||||
})
|
||||
|
||||
it('should return 400 when subscription ID is missing', async () => {
|
||||
const req = createMockRequestWithParams({})
|
||||
req.body = { kinds: [1] }
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.createSubscription(req, res)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Subscription ID is required')
|
||||
})
|
||||
|
||||
it('should return 400 when filters are missing', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.body = {}
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.createSubscription(req, res)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Filters are required')
|
||||
})
|
||||
|
||||
it('should handle filters as array', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.body = [{ kinds: [1] }, { kinds: [3] }]
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.manageSubscription.createSubscription.resolves()
|
||||
|
||||
await uut.createSubscription(req, res)
|
||||
|
||||
// Assert filters array was passed correctly
|
||||
const createArgs = mockUseCases.manageSubscription.createSubscription.getCall(0).args
|
||||
assert.isArray(createArgs[1])
|
||||
assert.equal(createArgs[1].length, 2)
|
||||
})
|
||||
|
||||
it('should handle client disconnect', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.body = { kinds: [1] }
|
||||
req.on = sinon.stub()
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.manageSubscription.createSubscription.resolves()
|
||||
mockUseCases.manageSubscription.closeSubscription.resolves()
|
||||
|
||||
await uut.createSubscription(req, res)
|
||||
|
||||
// Assert close handler was set up
|
||||
assert.isTrue(req.on.calledWith('close'))
|
||||
|
||||
// Simulate client disconnect
|
||||
const closeCallback = req.on.getCall(0).args[1]
|
||||
await closeCallback()
|
||||
|
||||
// Assert closeSubscription was called
|
||||
assert.isTrue(mockUseCases.manageSubscription.closeSubscription.calledOnce)
|
||||
assert.equal(mockUseCases.manageSubscription.closeSubscription.getCall(0).args[0], 'test-sub-123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#closeSubscription()', () => {
|
||||
it('should successfully close a subscription', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.manageSubscription.closeSubscription.resolves()
|
||||
|
||||
await uut.closeSubscription(req, res)
|
||||
|
||||
// Assert use case was called
|
||||
assert.isTrue(mockUseCases.manageSubscription.closeSubscription.calledOnce)
|
||||
assert.equal(mockUseCases.manageSubscription.closeSubscription.getCall(0).args[0], 'test-sub-123')
|
||||
|
||||
// Assert response
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.property(res.jsonData, 'message')
|
||||
assert.include(res.jsonData.message, 'closed successfully')
|
||||
})
|
||||
|
||||
it('should return 400 when subscription ID is missing', async () => {
|
||||
const req = createMockRequestWithParams({})
|
||||
const res = createMockResponse()
|
||||
|
||||
await uut.closeSubscription(req, res)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 400)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Subscription ID is required')
|
||||
})
|
||||
|
||||
it('should handle use case errors', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.manageSubscription.closeSubscription.rejects(new Error('Relay connection error'))
|
||||
|
||||
await uut.closeSubscription(req, res)
|
||||
|
||||
// Assert error response
|
||||
assert.equal(res.statusValue, 500)
|
||||
assert.property(res.jsonData, 'error')
|
||||
assert.include(res.jsonData.error, 'Relay connection error')
|
||||
})
|
||||
|
||||
it('should handle idempotent close (subscription already closed)', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
const res = createMockResponse()
|
||||
|
||||
// closeSubscription resolves successfully even if subscription doesn't exist
|
||||
mockUseCases.manageSubscription.closeSubscription.resolves()
|
||||
|
||||
await uut.closeSubscription(req, res)
|
||||
|
||||
// Assert success response even for already-closed subscription
|
||||
assert.equal(res.statusValue, 200)
|
||||
assert.property(res.jsonData, 'message')
|
||||
assert.include(res.jsonData.message, 'closed successfully')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#createSubscriptionPut()', () => {
|
||||
it('should call createSubscription', async () => {
|
||||
const req = createMockRequestWithParams({ subId: 'test-sub-123' })
|
||||
req.body = { kinds: [1] }
|
||||
const res = createMockResponse()
|
||||
|
||||
mockUseCases.manageSubscription.createSubscription.resolves()
|
||||
|
||||
await uut.createSubscriptionPut(req, res)
|
||||
|
||||
// Assert createSubscription was called
|
||||
assert.isTrue(mockUseCases.manageSubscription.createSubscription.calledOnce)
|
||||
})
|
||||
})
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should require adapters instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new ReqRESTControllerLib({ useCases: mockUseCases })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Adapters library required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should require useCases instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new ReqRESTControllerLib({ adapters: {} })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Use Cases library required')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
Unit tests for the Event entity.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
|
||||
// Mocking data libraries
|
||||
import {
|
||||
mockKind0Event,
|
||||
mockKind1Event,
|
||||
mockKind3Event,
|
||||
mockKind7Event,
|
||||
mockInvalidEventMissingId,
|
||||
mockInvalidEventWrongIdLength,
|
||||
mockInvalidEventMissingPubkey,
|
||||
mockInvalidEventWrongPubkeyLength,
|
||||
mockInvalidEventMissingCreatedAt,
|
||||
mockInvalidEventWrongCreatedAtType,
|
||||
mockInvalidEventMissingKind,
|
||||
mockInvalidEventKindOutOfRange,
|
||||
mockInvalidEventMissingSig,
|
||||
mockInvalidEventWrongSigLength,
|
||||
mockInvalidEventTagsNotArray
|
||||
} from '../mocks/event-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import Event from '../../../src/entities/event.js'
|
||||
|
||||
describe('#event.js', () => {
|
||||
describe('#isValid()', () => {
|
||||
it('should return true for valid kind 0 event', () => {
|
||||
const event = new Event(mockKind0Event)
|
||||
assert.isTrue(event.isValid())
|
||||
})
|
||||
|
||||
it('should return true for valid kind 1 event', () => {
|
||||
const event = new Event(mockKind1Event)
|
||||
assert.isTrue(event.isValid())
|
||||
})
|
||||
|
||||
it('should return true for valid kind 3 event', () => {
|
||||
const event = new Event(mockKind3Event)
|
||||
assert.isTrue(event.isValid())
|
||||
})
|
||||
|
||||
it('should return true for valid kind 7 event', () => {
|
||||
const event = new Event(mockKind7Event)
|
||||
assert.isTrue(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event missing id', () => {
|
||||
const event = new Event(mockInvalidEventMissingId)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event with wrong id length', () => {
|
||||
const event = new Event(mockInvalidEventWrongIdLength)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event missing pubkey', () => {
|
||||
const event = new Event(mockInvalidEventMissingPubkey)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event with wrong pubkey length', () => {
|
||||
const event = new Event(mockInvalidEventWrongPubkeyLength)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event missing created_at', () => {
|
||||
const event = new Event(mockInvalidEventMissingCreatedAt)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event with wrong created_at type', () => {
|
||||
const event = new Event(mockInvalidEventWrongCreatedAtType)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event missing kind', () => {
|
||||
const event = new Event(mockInvalidEventMissingKind)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event with kind out of range', () => {
|
||||
const event = new Event(mockInvalidEventKindOutOfRange)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event missing sig', () => {
|
||||
const event = new Event(mockInvalidEventMissingSig)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event with wrong sig length', () => {
|
||||
const event = new Event(mockInvalidEventWrongSigLength)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
|
||||
it('should return false for event with tags not an array', () => {
|
||||
const event = new Event(mockInvalidEventTagsNotArray)
|
||||
assert.isFalse(event.isValid())
|
||||
})
|
||||
})
|
||||
|
||||
describe('#toJSON()', () => {
|
||||
it('should serialize event to JSON correctly', () => {
|
||||
const event = new Event(mockKind1Event)
|
||||
const json = event.toJSON()
|
||||
|
||||
assert.property(json, 'id')
|
||||
assert.property(json, 'pubkey')
|
||||
assert.property(json, 'created_at')
|
||||
assert.property(json, 'kind')
|
||||
assert.property(json, 'tags')
|
||||
assert.property(json, 'content')
|
||||
assert.property(json, 'sig')
|
||||
|
||||
assert.equal(json.id, mockKind1Event.id)
|
||||
assert.equal(json.pubkey, mockKind1Event.pubkey)
|
||||
assert.equal(json.created_at, mockKind1Event.created_at)
|
||||
assert.equal(json.kind, mockKind1Event.kind)
|
||||
assert.deepEqual(json.tags, mockKind1Event.tags)
|
||||
assert.equal(json.content, mockKind1Event.content)
|
||||
assert.equal(json.sig, mockKind1Event.sig)
|
||||
})
|
||||
|
||||
it('should serialize event with tags correctly', () => {
|
||||
const event = new Event(mockKind3Event)
|
||||
const json = event.toJSON()
|
||||
|
||||
assert.isArray(json.tags)
|
||||
assert.equal(json.tags.length, 1)
|
||||
assert.deepEqual(json.tags, mockKind3Event.tags)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Mock Express request/response objects for controller unit tests.
|
||||
*/
|
||||
|
||||
// Mock Express request object
|
||||
export function createMockRequest (overrides = {}) {
|
||||
return {
|
||||
body: {},
|
||||
params: {},
|
||||
query: {},
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
// Mock Express response object
|
||||
export function createMockResponse () {
|
||||
const res = {
|
||||
statusCode: 200,
|
||||
jsonData: null,
|
||||
statusValue: null,
|
||||
headers: {},
|
||||
writeData: [],
|
||||
endCalled: false,
|
||||
writable: true, // Stream is writable by default
|
||||
destroyed: false, // Stream is not destroyed by default
|
||||
closed: false, // Stream is not closed by default
|
||||
eventHandlers: {} // Store event handlers
|
||||
}
|
||||
|
||||
res.status = function (code) {
|
||||
res.statusCode = code
|
||||
res.statusValue = code
|
||||
return res
|
||||
}
|
||||
|
||||
res.json = function (data) {
|
||||
res.jsonData = data
|
||||
return res
|
||||
}
|
||||
|
||||
res.setHeader = function (name, value) {
|
||||
res.headers[name] = value
|
||||
return res
|
||||
}
|
||||
|
||||
res.write = function (data) {
|
||||
res.writeData.push(data)
|
||||
return true
|
||||
}
|
||||
|
||||
res.end = function () {
|
||||
res.endCalled = true
|
||||
return res
|
||||
}
|
||||
|
||||
res.on = function (event, callback) {
|
||||
// Store event handlers for different event types
|
||||
if (!res.eventHandlers[event]) {
|
||||
res.eventHandlers[event] = []
|
||||
}
|
||||
res.eventHandlers[event].push(callback)
|
||||
|
||||
// For backward compatibility with existing tests
|
||||
if (event === 'close') {
|
||||
res.closeCallback = callback
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Helper to trigger an event (useful for testing)
|
||||
res.trigger = function (event, ...args) {
|
||||
if (res.eventHandlers[event]) {
|
||||
for (const handler of res.eventHandlers[event]) {
|
||||
handler(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Helper to create a mock request with body
|
||||
export function createMockRequestWithBody (body) {
|
||||
return createMockRequest({ body })
|
||||
}
|
||||
|
||||
// Helper to create a mock request with params
|
||||
export function createMockRequestWithParams (params) {
|
||||
return createMockRequest({ params })
|
||||
}
|
||||
|
||||
// Helper to create a mock request with query
|
||||
export function createMockRequestWithQuery (query) {
|
||||
return createMockRequest({ query })
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
Mock event data for unit tests.
|
||||
Contains mock Nostr events for various event kinds.
|
||||
*/
|
||||
|
||||
// Alice's public key from examples
|
||||
const alicePubKey = '2c7e76c0f8dc1dca9d0197c7d19be580a8d074ccada6a2f6ebe056ae41092e92'
|
||||
const bobPubKey = 'b'.repeat(64)
|
||||
|
||||
// Valid event ID (64 hex chars)
|
||||
const validEventId = 'd09b4c5da59be3cd2768aa53fa78b77bf4859084c94f3bf26d401f004a9c8167'
|
||||
// Valid signature (128 hex chars)
|
||||
const validSig = 'a'.repeat(128)
|
||||
|
||||
// Kind 0: Profile metadata event
|
||||
const mockKind0Event = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 0,
|
||||
tags: [],
|
||||
content: JSON.stringify({
|
||||
name: 'Alice',
|
||||
about: 'Hello, I am Alice!',
|
||||
picture: 'https://example.com/alice.jpg'
|
||||
}),
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
// Kind 1: Text post event
|
||||
const mockKind1Event = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'This is a test message',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
// Kind 3: Follow list event
|
||||
const mockKind3Event = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 3,
|
||||
tags: [
|
||||
['p', bobPubKey, 'wss://nostr-relay.psfoundation.info', 'bob']
|
||||
],
|
||||
content: '',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
// Kind 7: Reaction/like event
|
||||
const mockKind7Event = {
|
||||
id: validEventId,
|
||||
pubkey: bobPubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 7,
|
||||
tags: [
|
||||
['e', validEventId, 'wss://nostr-relay.psfoundation.info'],
|
||||
['p', alicePubKey, 'wss://nostr-relay.psfoundation.info']
|
||||
],
|
||||
content: '+',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
// Invalid events for testing validation
|
||||
const mockInvalidEventMissingId = {
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
const mockInvalidEventWrongIdLength = {
|
||||
id: 'short',
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
const mockInvalidEventMissingPubkey = {
|
||||
id: validEventId,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
const mockInvalidEventWrongPubkeyLength = {
|
||||
id: validEventId,
|
||||
pubkey: 'short',
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
const mockInvalidEventMissingCreatedAt = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
const mockInvalidEventWrongCreatedAtType = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: 'not-a-number',
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
const mockInvalidEventMissingKind = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
const mockInvalidEventKindOutOfRange = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 70000, // Out of range (0-65535)
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
const mockInvalidEventMissingSig = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test'
|
||||
}
|
||||
|
||||
const mockInvalidEventWrongSigLength = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: [],
|
||||
content: 'Test',
|
||||
sig: 'short'
|
||||
}
|
||||
|
||||
const mockInvalidEventTagsNotArray = {
|
||||
id: validEventId,
|
||||
pubkey: alicePubKey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 1,
|
||||
tags: 'not-an-array',
|
||||
content: 'Test',
|
||||
sig: validSig
|
||||
}
|
||||
|
||||
export {
|
||||
mockKind0Event,
|
||||
mockKind1Event,
|
||||
mockKind3Event,
|
||||
mockKind7Event,
|
||||
mockInvalidEventMissingId,
|
||||
mockInvalidEventWrongIdLength,
|
||||
mockInvalidEventMissingPubkey,
|
||||
mockInvalidEventWrongPubkeyLength,
|
||||
mockInvalidEventMissingCreatedAt,
|
||||
mockInvalidEventWrongCreatedAtType,
|
||||
mockInvalidEventMissingKind,
|
||||
mockInvalidEventKindOutOfRange,
|
||||
mockInvalidEventMissingSig,
|
||||
mockInvalidEventWrongSigLength,
|
||||
mockInvalidEventTagsNotArray,
|
||||
alicePubKey,
|
||||
bobPubKey,
|
||||
validEventId,
|
||||
validSig
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
Mock responses from Nostr relay for unit tests.
|
||||
Contains mock messages that would come from a Nostr relay WebSocket.
|
||||
*/
|
||||
|
||||
import { mockKind1Event, validEventId } from './event-mocks.js'
|
||||
|
||||
// Mock OK response (event accepted)
|
||||
const mockOkAccepted = ['OK', validEventId, true, 'event saved']
|
||||
|
||||
// Mock OK response (event rejected)
|
||||
const mockOkRejected = ['OK', validEventId, false, 'duplicate: event already exists']
|
||||
|
||||
// Mock EVENT message (from relay)
|
||||
const mockEventMessage = ['EVENT', 'subscription-id-123', mockKind1Event]
|
||||
|
||||
// Mock EOSE message (end of stored events)
|
||||
const mockEoseMessage = ['EOSE', 'subscription-id-123']
|
||||
|
||||
// Mock CLOSED message
|
||||
const mockClosedMessage = ['CLOSED', 'subscription-id-123', 'subscription closed']
|
||||
|
||||
// Mock NOTICE message
|
||||
const mockNoticeMessage = ['NOTICE', 'rate limited: slow down']
|
||||
|
||||
// Mock successful sendEvent response
|
||||
const mockSendEventSuccess = {
|
||||
accepted: true,
|
||||
message: 'event saved'
|
||||
}
|
||||
|
||||
// Mock failed sendEvent response
|
||||
const mockSendEventFailure = {
|
||||
accepted: false,
|
||||
message: 'duplicate: event already exists'
|
||||
}
|
||||
|
||||
// Mock events array for query tests
|
||||
const mockEventsArray = [
|
||||
mockKind1Event,
|
||||
{
|
||||
...mockKind1Event,
|
||||
id: 'b'.repeat(64),
|
||||
content: 'Another test message'
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
mockOkAccepted,
|
||||
mockOkRejected,
|
||||
mockEventMessage,
|
||||
mockEoseMessage,
|
||||
mockClosedMessage,
|
||||
mockNoticeMessage,
|
||||
mockSendEventSuccess,
|
||||
mockSendEventFailure,
|
||||
mockEventsArray
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
Unit tests for ManageSubscriptionUseCase.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import { mockKind1Event } from '../mocks/event-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import ManageSubscriptionUseCase from '../../../src/use-cases/manage-subscription.js'
|
||||
|
||||
describe('#manage-subscription.js', () => {
|
||||
let sandbox
|
||||
let mockAdapters
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock adapters with multiple relays support
|
||||
const mockRelay1 = {
|
||||
relayUrl: 'wss://relay1.example.com',
|
||||
sendReq: sandbox.stub(),
|
||||
sendClose: sandbox.stub()
|
||||
}
|
||||
const mockRelay2 = {
|
||||
relayUrl: 'wss://relay2.example.com',
|
||||
sendReq: sandbox.stub(),
|
||||
sendClose: sandbox.stub()
|
||||
}
|
||||
|
||||
mockAdapters = {
|
||||
nostrRelays: [mockRelay1, mockRelay2]
|
||||
}
|
||||
|
||||
uut = new ManageSubscriptionUseCase({ adapters: mockAdapters })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#createSubscription()', () => {
|
||||
it('should successfully create a subscription across all relays', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
let onEventCalled = false
|
||||
let onEoseCalled = false
|
||||
let onClosedCalled = false
|
||||
|
||||
const onEvent = (event) => {
|
||||
onEventCalled = true
|
||||
}
|
||||
const onEose = () => {
|
||||
onEoseCalled = true
|
||||
}
|
||||
const onClosed = (message) => {
|
||||
onClosedCalled = true
|
||||
}
|
||||
|
||||
// Mock adapters to resolve
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters, onEvent, onEose, onClosed)
|
||||
|
||||
// Assert adapters were called for both relays
|
||||
assert.isTrue(mockAdapters.nostrRelays[0].sendReq.calledOnce)
|
||||
assert.isTrue(mockAdapters.nostrRelays[1].sendReq.calledOnce)
|
||||
|
||||
// Assert subscription is tracked
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
|
||||
// Test handlers - get them from the subscription info
|
||||
const subscriptionInfo = uut.activeSubscriptions.get(subscriptionId)
|
||||
const handlers = subscriptionInfo.handlers
|
||||
|
||||
// Test event handler (should de-duplicate)
|
||||
handlers.onEvent(mockKind1Event)
|
||||
assert.isTrue(onEventCalled)
|
||||
|
||||
// Simulate EOSE from both relays
|
||||
const relayStatuses = subscriptionInfo.relayStatuses
|
||||
relayStatuses[0].eoseReceived = true
|
||||
relayStatuses[1].eoseReceived = true
|
||||
handlers.onEose()
|
||||
assert.isTrue(onEoseCalled)
|
||||
|
||||
handlers.onClosed('test message')
|
||||
assert.isTrue(onClosedCalled)
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
|
||||
it('should prevent duplicate subscriptions', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
|
||||
try {
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'already exists')
|
||||
}
|
||||
})
|
||||
|
||||
it('should clean up subscription on error', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.rejects(new Error('Connection error'))
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
try {
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
// Should clean up even if some relays fail
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle missing callbacks gracefully', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters, null, null, null)
|
||||
|
||||
// Should not throw when handlers are null
|
||||
const subscriptionInfo = uut.activeSubscriptions.get(subscriptionId)
|
||||
const handlers = subscriptionInfo.handlers
|
||||
handlers.onEvent(mockKind1Event)
|
||||
handlers.onEose()
|
||||
handlers.onClosed('test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#closeSubscription()', () => {
|
||||
it('should successfully close a subscription across all relays', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[0].sendClose.resolves()
|
||||
mockAdapters.nostrRelays[1].sendClose.resolves()
|
||||
|
||||
// Create subscription first
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
|
||||
// Close subscription
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
|
||||
// Assert adapters were called for both relays
|
||||
assert.isTrue(mockAdapters.nostrRelays[0].sendClose.calledOnce)
|
||||
assert.isTrue(mockAdapters.nostrRelays[1].sendClose.calledOnce)
|
||||
|
||||
// Assert subscription is removed
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
|
||||
it('should return successfully when closing non-existent subscription (idempotent)', async () => {
|
||||
const subscriptionId = 'non-existent-sub'
|
||||
|
||||
// Should not throw - idempotent operation
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
|
||||
// Should return successfully without error
|
||||
assert.isTrue(true, 'closeSubscription should succeed for non-existent subscription')
|
||||
})
|
||||
|
||||
it('should clean up subscription even on error', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[0].sendClose.rejects(new Error('Close error'))
|
||||
mockAdapters.nostrRelays[1].sendClose.resolves()
|
||||
|
||||
// Create subscription first
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
|
||||
// Close should succeed even if one relay fails
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
|
||||
// Should still clean up
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hasSubscription()', () => {
|
||||
it('should return false for non-existent subscription', () => {
|
||||
assert.isFalse(uut.hasSubscription('non-existent'))
|
||||
})
|
||||
|
||||
it('should return true for existing subscription', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should require adapters instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new ManageSubscriptionUseCase()
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Adapters instance required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should require NostrRelay adapters array', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new ManageSubscriptionUseCase({ adapters: {} })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'NostrRelay adapters array required')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Unit tests for PublishEventUseCase.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import {
|
||||
mockKind1Event,
|
||||
mockInvalidEventMissingId
|
||||
} from '../mocks/event-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import PublishEventUseCase from '../../../src/use-cases/publish-event.js'
|
||||
|
||||
describe('#publish-event.js', () => {
|
||||
let sandbox
|
||||
let mockAdapters
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock adapters with multiple relays support
|
||||
mockAdapters = {
|
||||
nostrRelays: [
|
||||
{ relayUrl: 'wss://relay1.example.com' },
|
||||
{ relayUrl: 'wss://relay2.example.com' }
|
||||
],
|
||||
broadcastEvent: sandbox.stub()
|
||||
}
|
||||
|
||||
uut = new PublishEventUseCase({ adapters: mockAdapters })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#execute()', () => {
|
||||
it('should successfully publish a valid event to all relays', async () => {
|
||||
// Mock broadcast response - at least one relay accepts
|
||||
mockAdapters.broadcastEvent.resolves([
|
||||
{ accepted: true, message: 'event saved', relayUrl: 'wss://relay1.example.com', success: true },
|
||||
{ accepted: true, message: 'event saved', relayUrl: 'wss://relay2.example.com', success: true }
|
||||
])
|
||||
|
||||
const result = await uut.execute(mockKind1Event)
|
||||
|
||||
// Assert adapter was called correctly
|
||||
assert.isTrue(mockAdapters.broadcastEvent.calledOnce)
|
||||
const callArgs = mockAdapters.broadcastEvent.getCall(0).args[0]
|
||||
assert.equal(callArgs.id, mockKind1Event.id)
|
||||
assert.equal(callArgs.kind, mockKind1Event.kind)
|
||||
|
||||
// Assert result
|
||||
assert.property(result, 'accepted')
|
||||
assert.property(result, 'message')
|
||||
assert.property(result, 'eventId')
|
||||
assert.property(result, 'relayResults')
|
||||
assert.property(result, 'acceptedCount')
|
||||
assert.property(result, 'totalRelays')
|
||||
assert.isTrue(result.accepted)
|
||||
assert.equal(result.eventId, mockKind1Event.id)
|
||||
assert.equal(result.acceptedCount, 2)
|
||||
assert.equal(result.totalRelays, 2)
|
||||
})
|
||||
|
||||
it('should handle event rejection from all relays', async () => {
|
||||
// Mock broadcast response - all relays reject
|
||||
mockAdapters.broadcastEvent.resolves([
|
||||
{ accepted: false, message: 'duplicate', relayUrl: 'wss://relay1.example.com', success: true },
|
||||
{ accepted: false, message: 'duplicate', relayUrl: 'wss://relay2.example.com', success: true }
|
||||
])
|
||||
|
||||
const result = await uut.execute(mockKind1Event)
|
||||
|
||||
// Assert result shows rejection
|
||||
assert.isFalse(result.accepted)
|
||||
assert.property(result, 'message')
|
||||
assert.equal(result.eventId, mockKind1Event.id)
|
||||
assert.equal(result.acceptedCount, 0)
|
||||
})
|
||||
|
||||
it('should succeed if at least one relay accepts', async () => {
|
||||
// Mock broadcast response - one accepts, one rejects
|
||||
mockAdapters.broadcastEvent.resolves([
|
||||
{ accepted: true, message: 'event saved', relayUrl: 'wss://relay1.example.com', success: true },
|
||||
{ accepted: false, message: 'duplicate', relayUrl: 'wss://relay2.example.com', success: true }
|
||||
])
|
||||
|
||||
const result = await uut.execute(mockKind1Event)
|
||||
|
||||
// Should succeed if at least one accepts
|
||||
assert.isTrue(result.accepted)
|
||||
assert.equal(result.acceptedCount, 1)
|
||||
assert.equal(result.totalRelays, 2)
|
||||
})
|
||||
|
||||
it('should throw error for invalid event structure', async () => {
|
||||
try {
|
||||
await uut.execute(mockInvalidEventMissingId)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Invalid event structure')
|
||||
assert.isFalse(mockAdapters.broadcastEvent.called)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle adapter errors', async () => {
|
||||
// Mock adapter error
|
||||
const adapterError = new Error('Network error')
|
||||
mockAdapters.broadcastEvent.rejects(adapterError)
|
||||
|
||||
try {
|
||||
await uut.execute(mockKind1Event)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.message, 'Network error')
|
||||
assert.isTrue(mockAdapters.broadcastEvent.calledOnce)
|
||||
}
|
||||
})
|
||||
|
||||
it('should require adapters instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new PublishEventUseCase()
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Adapters instance required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should require NostrRelay adapters array', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new PublishEventUseCase({ adapters: {} })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'NostrRelay adapters array required')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Unit tests for QueryEventsUseCase.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import { mockEventsArray } from '../mocks/nostr-relay-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import QueryEventsUseCase from '../../../src/use-cases/query-events.js'
|
||||
|
||||
describe('#query-events.js', () => {
|
||||
let sandbox
|
||||
let mockAdapters
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock adapters with multiple relays support
|
||||
mockAdapters = {
|
||||
nostrRelays: [
|
||||
{ relayUrl: 'wss://relay1.example.com' },
|
||||
{ relayUrl: 'wss://relay2.example.com' }
|
||||
],
|
||||
queryAllRelays: sandbox.stub()
|
||||
}
|
||||
|
||||
uut = new QueryEventsUseCase({ adapters: mockAdapters })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#execute()', () => {
|
||||
it('should successfully query events from all relays and return merged results', async () => {
|
||||
const filters = [{ kinds: [1], limit: 10 }]
|
||||
const subscriptionId = 'test-sub-123'
|
||||
|
||||
// Mock queryAllRelays to return events
|
||||
mockAdapters.queryAllRelays.resolves(mockEventsArray)
|
||||
|
||||
const result = await uut.execute(filters, subscriptionId)
|
||||
|
||||
// Assert adapter was called correctly
|
||||
assert.isTrue(mockAdapters.queryAllRelays.calledOnce)
|
||||
const callArgs = mockAdapters.queryAllRelays.getCall(0).args
|
||||
assert.deepEqual(callArgs[0], filters)
|
||||
assert.equal(callArgs[1], subscriptionId)
|
||||
|
||||
// Assert result contains events
|
||||
assert.isArray(result)
|
||||
assert.equal(result.length, mockEventsArray.length)
|
||||
})
|
||||
|
||||
it('should handle errors from queryAllRelays', async () => {
|
||||
const filters = [{ kinds: [1] }]
|
||||
const subscriptionId = 'test-sub-123'
|
||||
|
||||
mockAdapters.queryAllRelays.rejects(new Error('Query failed'))
|
||||
|
||||
try {
|
||||
await uut.execute(filters, subscriptionId)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Query failed')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return empty array when no events found', async () => {
|
||||
const filters = [{ kinds: [1] }]
|
||||
const subscriptionId = 'test-sub-123'
|
||||
|
||||
mockAdapters.queryAllRelays.resolves([])
|
||||
|
||||
const result = await uut.execute(filters, subscriptionId)
|
||||
|
||||
assert.isArray(result)
|
||||
assert.equal(result.length, 0)
|
||||
})
|
||||
|
||||
it('should require adapters instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new QueryEventsUseCase()
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Adapters instance required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should require NostrRelay adapters array', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new QueryEventsUseCase({ adapters: {} })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'NostrRelay adapters array required')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user