first commit

This commit is contained in:
Chris Troutner
2025-11-09 16:16:37 -08:00
commit ba2afd47a4
61 changed files with 13847 additions and 0 deletions
+250
View File
@@ -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')
})
})
})
+173
View File
@@ -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)
})
})
})
+304
View File
@@ -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)
})
})
})
*/
+63
View File
@@ -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')
}
})
})
})
+139
View File
@@ -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)
})
})
})
+98
View File
@@ -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 })
}
+194
View File
@@ -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
}
+58
View File
@@ -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')
}
})
})
})
+146
View File
@@ -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')
}
})
})
})
+106
View File
@@ -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')
}
})
})
})