mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api.git
synced 2026-09-21 16:52:00 -07:00
fix(tests): Updating unit tests
This commit is contained in:
@@ -20,7 +20,6 @@ class FullNodeRPCAdapter {
|
|||||||
rpcPassword,
|
rpcPassword,
|
||||||
rpcTimeoutMs = 15000
|
rpcTimeoutMs = 15000
|
||||||
} = this.config.fullNode
|
} = this.config.fullNode
|
||||||
console.log('this.config.fullNode', this.config.fullNode)
|
|
||||||
|
|
||||||
this.requestIdPrefix = this.config.fullNode.rpcRequestIdPrefix || 'psf-bch-api'
|
this.requestIdPrefix = this.config.fullNode.rpcRequestIdPrefix || 'psf-bch-api'
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Local libraries
|
// Local libraries
|
||||||
// import PublishEventUseCase from './publish-event.js'
|
|
||||||
// import QueryEventsUseCase from './query-events.js'
|
|
||||||
// import ManageSubscriptionUseCase from './manage-subscription.js'
|
|
||||||
import BlockchainUseCases from './full-node-blockchain-use-cases.js'
|
import BlockchainUseCases from './full-node-blockchain-use-cases.js'
|
||||||
|
|
||||||
class UseCases {
|
class UseCases {
|
||||||
@@ -19,9 +16,6 @@ class UseCases {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.publishEvent = new PublishEventUseCase({ adapters: this.adapters })
|
|
||||||
// this.queryEvents = new QueryEventsUseCase({ adapters: this.adapters })
|
|
||||||
// this.manageSubscription = new ManageSubscriptionUseCase({ adapters: this.adapters })
|
|
||||||
this.blockchain = new BlockchainUseCases({ adapters: this.adapters })
|
this.blockchain = new BlockchainUseCases({ adapters: this.adapters })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,216 +0,0 @@
|
|||||||
/*
|
|
||||||
Use case: Manage subscriptions for Server-Sent Events (SSE).
|
|
||||||
This encapsulates the business logic for creating and managing subscriptions.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import wlogger from '../adapters/wlogger.js'
|
|
||||||
|
|
||||||
class ManageSubscriptionUseCase {
|
|
||||||
constructor (localConfig = {}) {
|
|
||||||
this.adapters = localConfig.adapters
|
|
||||||
if (!this.adapters) {
|
|
||||||
throw new Error('Adapters instance required')
|
|
||||||
}
|
|
||||||
if (!this.adapters.nostrRelays || !Array.isArray(this.adapters.nostrRelays) || this.adapters.nostrRelays.length === 0) {
|
|
||||||
throw new Error('NostrRelay adapters array required')
|
|
||||||
}
|
|
||||||
// Map subscriptionId to { relaySubscriptions: Map<relayIndex, subscriptionId>, handlers, seenEventIds }
|
|
||||||
this.activeSubscriptions = new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a subscription for SSE streaming across all relays
|
|
||||||
* @param {string} subscriptionId - Unique subscription ID
|
|
||||||
* @param {Array} filters - Array of filter objects
|
|
||||||
* @param {Function} onEvent - Callback for events
|
|
||||||
* @param {Function} onEose - Callback for EOSE
|
|
||||||
* @param {Function} onClosed - Callback for CLOSED
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async createSubscription (subscriptionId, filters, onEvent, onEose, onClosed) {
|
|
||||||
try {
|
|
||||||
if (this.activeSubscriptions.has(subscriptionId)) {
|
|
||||||
throw new Error(`Subscription ${subscriptionId} already exists`)
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.info(`Creating subscription ${subscriptionId} across ${this.adapters.nostrRelays.length} relay(s)`)
|
|
||||||
|
|
||||||
// Track seen event IDs to de-duplicate across relays
|
|
||||||
const seenEventIds = new Set()
|
|
||||||
|
|
||||||
// Track EOSE and CLOSED status per relay
|
|
||||||
const relayStatuses = this.adapters.nostrRelays.map(() => ({
|
|
||||||
eoseReceived: false,
|
|
||||||
closedReceived: false
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Create unified handlers that merge events from all relays
|
|
||||||
const handlers = {
|
|
||||||
onEvent: (event) => {
|
|
||||||
// De-duplicate events by ID across all relays
|
|
||||||
if (event && event.id && !seenEventIds.has(event.id)) {
|
|
||||||
seenEventIds.add(event.id)
|
|
||||||
if (onEvent) {
|
|
||||||
onEvent(event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onEose: () => {
|
|
||||||
// Call onEose only once when all relays have sent EOSE
|
|
||||||
// This is called from the per-relay handler only when all relays have EOSE
|
|
||||||
if (onEose) {
|
|
||||||
onEose()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClosed: (message) => {
|
|
||||||
if (onClosed) {
|
|
||||||
onClosed(message)
|
|
||||||
}
|
|
||||||
// Clean up subscription if any relay closes it
|
|
||||||
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
|
|
||||||
if (subscriptionInfo && subscriptionInfo.eoseTimeoutId) {
|
|
||||||
clearTimeout(subscriptionInfo.eoseTimeoutId)
|
|
||||||
}
|
|
||||||
this.activeSubscriptions.delete(subscriptionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create subscription per relay with unique subscription IDs
|
|
||||||
const relaySubscriptions = new Map()
|
|
||||||
const subscriptionPromises = this.adapters.nostrRelays.map(async (relay, index) => {
|
|
||||||
const relaySubscriptionId = `${subscriptionId}-relay-${index}`
|
|
||||||
relaySubscriptions.set(index, relaySubscriptionId)
|
|
||||||
|
|
||||||
// Create per-relay handlers that update shared state
|
|
||||||
const relayHandlers = {
|
|
||||||
onEvent: (event) => {
|
|
||||||
handlers.onEvent(event)
|
|
||||||
},
|
|
||||||
onEose: () => {
|
|
||||||
relayStatuses[index].eoseReceived = true
|
|
||||||
// Check if all relays have sent EOSE
|
|
||||||
if (relayStatuses.every(s => s.eoseReceived)) {
|
|
||||||
// Clear the timeout since we got EOSE from all relays
|
|
||||||
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
|
|
||||||
if (subscriptionInfo && subscriptionInfo.eoseTimeoutId) {
|
|
||||||
clearTimeout(subscriptionInfo.eoseTimeoutId)
|
|
||||||
subscriptionInfo.eoseTimeoutId = null
|
|
||||||
}
|
|
||||||
handlers.onEose()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClosed: (message) => {
|
|
||||||
relayStatuses[index].closedReceived = true
|
|
||||||
handlers.onClosed(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await relay.sendReq(relaySubscriptionId, filters, relayHandlers)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Store subscription info
|
|
||||||
this.activeSubscriptions.set(subscriptionId, {
|
|
||||||
relaySubscriptions,
|
|
||||||
handlers,
|
|
||||||
seenEventIds,
|
|
||||||
relayStatuses,
|
|
||||||
eoseTimeoutId: null
|
|
||||||
})
|
|
||||||
|
|
||||||
// Subscribe to all relays concurrently
|
|
||||||
const results = await Promise.allSettled(subscriptionPromises)
|
|
||||||
|
|
||||||
// Check if any relay subscription failed and clean up if so
|
|
||||||
const hasFailures = results.some(result => result.status === 'rejected')
|
|
||||||
if (hasFailures) {
|
|
||||||
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
|
|
||||||
if (subscriptionInfo && subscriptionInfo.eoseTimeoutId) {
|
|
||||||
clearTimeout(subscriptionInfo.eoseTimeoutId)
|
|
||||||
}
|
|
||||||
this.activeSubscriptions.delete(subscriptionId)
|
|
||||||
const errors = results
|
|
||||||
.filter(result => result.status === 'rejected')
|
|
||||||
.map(result => result.reason)
|
|
||||||
throw new Error(`Failed to create subscription on some relays: ${errors.map(e => e.message).join(', ')}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up EOSE timeout fallback - if not all relays send EOSE within 10 seconds, call onEose anyway
|
|
||||||
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
|
|
||||||
const EOSE_TIMEOUT_MS = 10000 // 10 seconds
|
|
||||||
subscriptionInfo.eoseTimeoutId = setTimeout(() => {
|
|
||||||
// Check if subscription still exists and if all relays have sent EOSE
|
|
||||||
if (this.activeSubscriptions.has(subscriptionId)) {
|
|
||||||
const currentInfo = this.activeSubscriptions.get(subscriptionId)
|
|
||||||
const allEoseReceived = currentInfo.relayStatuses.every(s => s.eoseReceived)
|
|
||||||
if (!allEoseReceived) {
|
|
||||||
wlogger.warn(`EOSE timeout reached for subscription ${subscriptionId} - calling onEose callback anyway`)
|
|
||||||
if (handlers.onEose) {
|
|
||||||
handlers.onEose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, EOSE_TIMEOUT_MS)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error creating subscription:', err)
|
|
||||||
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
|
|
||||||
if (subscriptionInfo && subscriptionInfo.eoseTimeoutId) {
|
|
||||||
clearTimeout(subscriptionInfo.eoseTimeoutId)
|
|
||||||
}
|
|
||||||
this.activeSubscriptions.delete(subscriptionId)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close a subscription across all relays
|
|
||||||
* @param {string} subscriptionId - Subscription ID to close
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async closeSubscription (subscriptionId) {
|
|
||||||
try {
|
|
||||||
if (!this.activeSubscriptions.has(subscriptionId)) {
|
|
||||||
// Subscription doesn't exist - already closed, treat as success (idempotent)
|
|
||||||
wlogger.info(`Subscription ${subscriptionId} already closed or does not exist`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.info(`Closing subscription ${subscriptionId} across all relays`)
|
|
||||||
|
|
||||||
const subscriptionInfo = this.activeSubscriptions.get(subscriptionId)
|
|
||||||
const { relaySubscriptions } = subscriptionInfo
|
|
||||||
|
|
||||||
// Clear EOSE timeout if it exists
|
|
||||||
if (subscriptionInfo.eoseTimeoutId) {
|
|
||||||
clearTimeout(subscriptionInfo.eoseTimeoutId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close subscriptions on all relays concurrently
|
|
||||||
const closePromises = Array.from(relaySubscriptions.entries()).map(async ([relayIndex, relaySubscriptionId]) => {
|
|
||||||
try {
|
|
||||||
await this.adapters.nostrRelays[relayIndex].sendClose(relaySubscriptionId)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.warn(`Error closing subscription on relay ${relayIndex}:`, err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.allSettled(closePromises)
|
|
||||||
this.activeSubscriptions.delete(subscriptionId)
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error closing subscription:', err)
|
|
||||||
// Clean up even if there's an error
|
|
||||||
this.activeSubscriptions.delete(subscriptionId)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a subscription exists
|
|
||||||
* @param {string} subscriptionId - Subscription ID
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
hasSubscription (subscriptionId) {
|
|
||||||
return this.activeSubscriptions.has(subscriptionId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ManageSubscriptionUseCase
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
Use case: Publish a Nostr event to the relay.
|
|
||||||
This encapsulates the business logic for publishing events.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import Event from '../entities/event.js'
|
|
||||||
import wlogger from '../adapters/wlogger.js'
|
|
||||||
|
|
||||||
class PublishEventUseCase {
|
|
||||||
constructor (localConfig = {}) {
|
|
||||||
this.adapters = localConfig.adapters
|
|
||||||
if (!this.adapters) {
|
|
||||||
throw new Error('Adapters instance required')
|
|
||||||
}
|
|
||||||
if (!this.adapters.nostrRelays || !Array.isArray(this.adapters.nostrRelays) || this.adapters.nostrRelays.length === 0) {
|
|
||||||
throw new Error('NostrRelay adapters array required')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish an event to all Nostr relays (broadcast)
|
|
||||||
* @param {Object} eventData - Event data (must be signed)
|
|
||||||
* @returns {Promise<Object>} Result with accepted status, message, and relay results
|
|
||||||
*/
|
|
||||||
async execute (eventData) {
|
|
||||||
try {
|
|
||||||
// Create event entity
|
|
||||||
const event = new Event(eventData)
|
|
||||||
|
|
||||||
// Validate event
|
|
||||||
if (!event.isValid()) {
|
|
||||||
throw new Error('Invalid event structure')
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.info(`Publishing event ${event.id} (kind ${event.kind}) to ${this.adapters.nostrRelays.length} relay(s)`)
|
|
||||||
|
|
||||||
// Broadcast event to all relays
|
|
||||||
const results = await this.adapters.broadcastEvent(event.toJSON())
|
|
||||||
|
|
||||||
// Aggregate results
|
|
||||||
const acceptedRelays = results.filter(r => r.accepted)
|
|
||||||
const rejectedRelays = results.filter(r => !r.accepted)
|
|
||||||
const failedRelays = results.filter(r => !r.success)
|
|
||||||
|
|
||||||
const atLeastOneAccepted = acceptedRelays.length > 0
|
|
||||||
const allAccepted = acceptedRelays.length === results.length && failedRelays.length === 0
|
|
||||||
|
|
||||||
// Build aggregated message
|
|
||||||
let message = ''
|
|
||||||
if (allAccepted) {
|
|
||||||
message = `Accepted by all ${acceptedRelays.length} relay(s)`
|
|
||||||
} else if (atLeastOneAccepted) {
|
|
||||||
message = `Accepted by ${acceptedRelays.length}/${results.length} relay(s)`
|
|
||||||
if (rejectedRelays.length > 0) {
|
|
||||||
message += `, rejected by ${rejectedRelays.length} relay(s)`
|
|
||||||
}
|
|
||||||
if (failedRelays.length > 0) {
|
|
||||||
message += `, failed to reach ${failedRelays.length} relay(s)`
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
message = `Rejected or failed by all ${results.length} relay(s)`
|
|
||||||
if (rejectedRelays.length > 0) {
|
|
||||||
const rejectionMessages = rejectedRelays.map(r => r.message).filter(m => m).join('; ')
|
|
||||||
if (rejectionMessages) {
|
|
||||||
message += `: ${rejectionMessages}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wlogger.info(`Event ${event.id} ${atLeastOneAccepted ? 'accepted' : 'rejected/failed'}: ${message}`)
|
|
||||||
|
|
||||||
return {
|
|
||||||
accepted: atLeastOneAccepted,
|
|
||||||
message,
|
|
||||||
eventId: event.id,
|
|
||||||
relayResults: results,
|
|
||||||
acceptedCount: acceptedRelays.length,
|
|
||||||
totalRelays: results.length
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in PublishEventUseCase:', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PublishEventUseCase
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
/*
|
|
||||||
Use case: Query events from the relay (stateless).
|
|
||||||
This encapsulates the business logic for querying events.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import wlogger from '../adapters/wlogger.js'
|
|
||||||
|
|
||||||
class QueryEventsUseCase {
|
|
||||||
constructor (localConfig = {}) {
|
|
||||||
this.adapters = localConfig.adapters
|
|
||||||
if (!this.adapters) {
|
|
||||||
throw new Error('Adapters instance required')
|
|
||||||
}
|
|
||||||
if (!this.adapters.nostrRelays || !Array.isArray(this.adapters.nostrRelays) || this.adapters.nostrRelays.length === 0) {
|
|
||||||
throw new Error('NostrRelay adapters array required')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query events with filters from all relays (stateless - returns immediately)
|
|
||||||
* @param {Array} filters - Array of filter objects
|
|
||||||
* @param {string} subscriptionId - Unique subscription ID
|
|
||||||
* @returns {Promise<Array>} Array of events (merged and de-duplicated from all relays)
|
|
||||||
*/
|
|
||||||
async execute (filters, subscriptionId) {
|
|
||||||
try {
|
|
||||||
wlogger.info(`Querying events with subscription ${subscriptionId} from ${this.adapters.nostrRelays.length} relay(s)`)
|
|
||||||
|
|
||||||
// Query all relays concurrently and merge results
|
|
||||||
const events = await this.adapters.queryAllRelays(filters, subscriptionId)
|
|
||||||
|
|
||||||
wlogger.info(`Query returned ${events.length} events from ${this.adapters.nostrRelays.length} relay(s)`)
|
|
||||||
return events
|
|
||||||
} catch (err) {
|
|
||||||
wlogger.error('Error in QueryEventsUseCase:', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default QueryEventsUseCase
|
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
Unit tests for FullNodeRPCAdapter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import FullNodeRPCAdapter from '../../../src/adapters/full-node-rpc.js'
|
||||||
|
|
||||||
|
describe('#full-node-rpc.js', () => {
|
||||||
|
let sandbox
|
||||||
|
let axiosCreateStub
|
||||||
|
let mockAxiosInstance
|
||||||
|
|
||||||
|
const baseConfig = {
|
||||||
|
fullNode: {
|
||||||
|
rpcBaseUrl: 'http://127.0.0.1:8332',
|
||||||
|
rpcUsername: 'user',
|
||||||
|
rpcPassword: 'pass',
|
||||||
|
rpcTimeoutMs: 1000,
|
||||||
|
rpcRequestIdPrefix: 'test'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
mockAxiosInstance = {
|
||||||
|
post: sandbox.stub()
|
||||||
|
}
|
||||||
|
axiosCreateStub = sandbox.stub(axios, 'create').returns(mockAxiosInstance)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#constructor()', () => {
|
||||||
|
it('should throw if full node config is missing', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new FullNodeRPCAdapter({ config: {} })
|
||||||
|
}, /Full node RPC configuration is required/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create axios client with provided configuration', () => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new FullNodeRPCAdapter({ config: baseConfig })
|
||||||
|
|
||||||
|
assert.isTrue(axiosCreateStub.calledOnce)
|
||||||
|
const options = axiosCreateStub.getCall(0).args[0]
|
||||||
|
assert.equal(options.baseURL, baseConfig.fullNode.rpcBaseUrl)
|
||||||
|
assert.equal(options.timeout, baseConfig.fullNode.rpcTimeoutMs)
|
||||||
|
assert.deepEqual(options.auth, {
|
||||||
|
username: baseConfig.fullNode.rpcUsername,
|
||||||
|
password: baseConfig.fullNode.rpcPassword
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#call()', () => {
|
||||||
|
it('should call RPC method and return result', async () => {
|
||||||
|
mockAxiosInstance.post.resolves({ data: { result: 'hash' } })
|
||||||
|
const uut = new FullNodeRPCAdapter({ config: baseConfig })
|
||||||
|
|
||||||
|
const result = await uut.call('getbestblockhash', [])
|
||||||
|
|
||||||
|
assert.equal(result, 'hash')
|
||||||
|
assert.isTrue(mockAxiosInstance.post.calledOnce)
|
||||||
|
const [, payload] = mockAxiosInstance.post.getCall(0).args
|
||||||
|
assert.deepEqual(payload, {
|
||||||
|
jsonrpc: '1.0',
|
||||||
|
id: 'test-getbestblockhash',
|
||||||
|
method: 'getbestblockhash',
|
||||||
|
params: []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use custom request id when provided', async () => {
|
||||||
|
mockAxiosInstance.post.resolves({ data: { result: 123 } })
|
||||||
|
const uut = new FullNodeRPCAdapter({ config: baseConfig })
|
||||||
|
|
||||||
|
await uut.call('getblockcount', [], 'custom-id')
|
||||||
|
|
||||||
|
const [, payload] = mockAxiosInstance.post.getCall(0).args
|
||||||
|
assert.equal(payload.id, 'custom-id')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw formatted error when RPC returns error', async () => {
|
||||||
|
mockAxiosInstance.post.resolves({
|
||||||
|
data: {
|
||||||
|
error: { message: 'RPC error' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const uut = new FullNodeRPCAdapter({ config: baseConfig })
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uut.call('failing', [])
|
||||||
|
assert.fail('Unexpected success')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.message, 'RPC error')
|
||||||
|
assert.equal(err.status, 400)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should translate network errors into 503 status', async () => {
|
||||||
|
mockAxiosInstance.post.rejects(new Error('ENOTFOUND fullnode'))
|
||||||
|
const uut = new FullNodeRPCAdapter({ config: baseConfig })
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uut.call('getblockcount', [])
|
||||||
|
assert.fail('Unexpected success')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(
|
||||||
|
err.message,
|
||||||
|
'Network error: Could not communicate with full node or other external service.'
|
||||||
|
)
|
||||||
|
assert.equal(err.status, 503)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
/*
|
|
||||||
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,215 @@
|
|||||||
|
/*
|
||||||
|
Unit tests for BlockchainRESTController.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
|
||||||
|
import BlockchainRESTController from '../../../src/controllers/rest-api/full-node/blockchain/controller.js'
|
||||||
|
import {
|
||||||
|
createMockRequest,
|
||||||
|
createMockResponse
|
||||||
|
} from '../mocks/controller-mocks.js'
|
||||||
|
|
||||||
|
describe('#blockchain-controller.js', () => {
|
||||||
|
let sandbox
|
||||||
|
let mockUseCases
|
||||||
|
let mockAdapters
|
||||||
|
let uut
|
||||||
|
|
||||||
|
const createBlockchainUseCaseStubs = () => ({
|
||||||
|
getBestBlockHash: sandbox.stub().resolves('hash'),
|
||||||
|
getBlockchainInfo: sandbox.stub().resolves({}),
|
||||||
|
getBlockCount: sandbox.stub().resolves(123),
|
||||||
|
getBlockHeader: sandbox.stub().resolves({ header: true }),
|
||||||
|
getBlockHeaders: sandbox.stub().resolves(['header']),
|
||||||
|
getChainTips: sandbox.stub().resolves(['tip']),
|
||||||
|
getDifficulty: sandbox.stub().resolves(1),
|
||||||
|
getMempoolEntry: sandbox.stub().resolves({}),
|
||||||
|
getMempoolEntries: sandbox.stub().resolves([]),
|
||||||
|
getMempoolAncestors: sandbox.stub().resolves([]),
|
||||||
|
getMempoolInfo: sandbox.stub().resolves({ size: 1 }),
|
||||||
|
getRawMempool: sandbox.stub().resolves(['tx']),
|
||||||
|
getTxOut: sandbox.stub().resolves({ value: 1 }),
|
||||||
|
getTxOutProof: sandbox.stub().resolves('proof'),
|
||||||
|
getTxOutProofs: sandbox.stub().resolves(['proof']),
|
||||||
|
verifyTxOutProof: sandbox.stub().resolves(['txid']),
|
||||||
|
verifyTxOutProofs: sandbox.stub().resolves([['txid']]),
|
||||||
|
getBlock: sandbox.stub().resolves({ hash: 'abc' }),
|
||||||
|
getBlockHash: sandbox.stub().resolves('blockhash')
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
mockAdapters = {
|
||||||
|
fullNode: {
|
||||||
|
validateArraySize: sandbox.stub().returns(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mockUseCases = {
|
||||||
|
blockchain: createBlockchainUseCaseStubs()
|
||||||
|
}
|
||||||
|
uut = new BlockchainRESTController({
|
||||||
|
adapters: mockAdapters,
|
||||||
|
useCases: mockUseCases
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#constructor()', () => {
|
||||||
|
it('should require adapters', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new BlockchainRESTController({ useCases: mockUseCases })
|
||||||
|
}, /Adapters library required/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require blockchain use cases', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new BlockchainRESTController({ adapters: mockAdapters, useCases: {} })
|
||||||
|
}, /Blockchain use cases required/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#root()', () => {
|
||||||
|
it('should return service status', async () => {
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
await uut.root(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 200)
|
||||||
|
assert.deepEqual(res.jsonData, { status: 'blockchain' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getBestBlockHash()', () => {
|
||||||
|
it('should return hash on success', async () => {
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
await uut.getBestBlockHash(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 200)
|
||||||
|
assert.equal(res.jsonData, 'hash')
|
||||||
|
assert.isTrue(mockUseCases.blockchain.getBestBlockHash.calledOnce)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle errors via handleError()', async () => {
|
||||||
|
const error = new Error('failure')
|
||||||
|
error.status = 422
|
||||||
|
mockUseCases.blockchain.getBestBlockHash.rejects(error)
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
await uut.getBestBlockHash(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 422)
|
||||||
|
assert.deepEqual(res.jsonData, { error: 'failure' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getBlockHeaderSingle()', () => {
|
||||||
|
it('should return 400 if hash is missing', async () => {
|
||||||
|
const req = createMockRequest()
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
await uut.getBlockHeaderSingle(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 400)
|
||||||
|
assert.property(res.jsonData, 'error')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call use case with verbose flag', async () => {
|
||||||
|
const hash = 'a'.repeat(64)
|
||||||
|
const req = createMockRequest({
|
||||||
|
params: { hash },
|
||||||
|
query: { verbose: 'true' }
|
||||||
|
})
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
await uut.getBlockHeaderSingle(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 200)
|
||||||
|
assert.isTrue(
|
||||||
|
mockUseCases.blockchain.getBlockHeader.calledOnceWithExactly({
|
||||||
|
hash,
|
||||||
|
verbose: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getBlockHeaderBulk()', () => {
|
||||||
|
it('should return error if hashes is not array', async () => {
|
||||||
|
const req = createMockRequest({
|
||||||
|
body: { hashes: 'not-an-array' },
|
||||||
|
locals: {}
|
||||||
|
})
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
await uut.getBlockHeaderBulk(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 400)
|
||||||
|
assert.include(res.jsonData.error, 'hashes needs to be an array')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should validate array size and call use case', async () => {
|
||||||
|
const hash = 'a'.repeat(64)
|
||||||
|
const req = createMockRequest({
|
||||||
|
body: { hashes: [hash], verbose: true },
|
||||||
|
locals: { proLimit: false }
|
||||||
|
})
|
||||||
|
const res = createMockResponse()
|
||||||
|
mockUseCases.blockchain.getBlockHeaders.resolves(['result'])
|
||||||
|
|
||||||
|
await uut.getBlockHeaderBulk(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 200)
|
||||||
|
assert.deepEqual(res.jsonData, ['result'])
|
||||||
|
assert.isTrue(
|
||||||
|
mockAdapters.fullNode.validateArraySize.calledOnceWithExactly(1, { isProUser: false })
|
||||||
|
)
|
||||||
|
assert.isTrue(
|
||||||
|
mockUseCases.blockchain.getBlockHeaders.calledOnceWithExactly({
|
||||||
|
hashes: [hash],
|
||||||
|
verbose: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error if array size invalid', async () => {
|
||||||
|
mockAdapters.fullNode.validateArraySize.returns(false)
|
||||||
|
const req = createMockRequest({
|
||||||
|
body: { hashes: ['a'.repeat(64)] },
|
||||||
|
locals: {}
|
||||||
|
})
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
await uut.getBlockHeaderBulk(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 400)
|
||||||
|
assert.equal(res.jsonData.error, 'Array too large.')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#verifyTxOutProofBulk()', () => {
|
||||||
|
it('should flatten proof responses', async () => {
|
||||||
|
mockUseCases.blockchain.verifyTxOutProofs.resolves([['txid-a'], ['txid-b']])
|
||||||
|
const req = createMockRequest({
|
||||||
|
body: { proofs: ['proof-a', 'proof-b'] },
|
||||||
|
locals: {}
|
||||||
|
})
|
||||||
|
const res = createMockResponse()
|
||||||
|
|
||||||
|
await uut.verifyTxOutProofBulk(req, res)
|
||||||
|
|
||||||
|
assert.equal(res.statusValue, 200)
|
||||||
|
assert.deepEqual(res.jsonData, ['txid-a', 'txid-b'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
/*
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
/*
|
|
||||||
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,85 @@
|
|||||||
|
/*
|
||||||
|
Unit tests for RESTControllers index.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
|
||||||
|
import RESTControllers from '../../../src/controllers/rest-api/index.js'
|
||||||
|
import BlockchainRouter from '../../../src/controllers/rest-api/full-node/blockchain/index.js'
|
||||||
|
|
||||||
|
describe('#controllers/rest-api/index.js', () => {
|
||||||
|
let sandbox
|
||||||
|
let mockAdapters
|
||||||
|
let mockUseCases
|
||||||
|
|
||||||
|
const createBlockchainUseCaseStubs = () => ({
|
||||||
|
getBestBlockHash: () => {},
|
||||||
|
getBlockchainInfo: () => {},
|
||||||
|
getBlockCount: () => {},
|
||||||
|
getBlockHeader: () => {},
|
||||||
|
getBlockHeaders: () => {},
|
||||||
|
getChainTips: () => {},
|
||||||
|
getDifficulty: () => {},
|
||||||
|
getMempoolEntry: () => {},
|
||||||
|
getMempoolEntries: () => {},
|
||||||
|
getMempoolAncestors: () => {},
|
||||||
|
getMempoolInfo: () => {},
|
||||||
|
getRawMempool: () => {},
|
||||||
|
getTxOut: () => {},
|
||||||
|
getTxOutProof: () => {},
|
||||||
|
getTxOutProofs: () => {},
|
||||||
|
verifyTxOutProof: () => {},
|
||||||
|
verifyTxOutProofs: () => {},
|
||||||
|
getBlock: () => {},
|
||||||
|
getBlockHash: () => {}
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
mockAdapters = {
|
||||||
|
fullNode: {
|
||||||
|
validateArraySize: sandbox.stub().returns(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mockUseCases = {
|
||||||
|
blockchain: createBlockchainUseCaseStubs()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#constructor()', () => {
|
||||||
|
it('should require adapters instance', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new RESTControllers({ useCases: mockUseCases })
|
||||||
|
}, /Adapters library required/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require useCases instance', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new RESTControllers({ adapters: mockAdapters })
|
||||||
|
}, /Use Cases library required/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#attachRESTControllers()', () => {
|
||||||
|
it('should instantiate blockchain router and attach to app', () => {
|
||||||
|
const attachStub = sandbox.stub(BlockchainRouter.prototype, 'attach')
|
||||||
|
const restControllers = new RESTControllers({
|
||||||
|
adapters: mockAdapters,
|
||||||
|
useCases: mockUseCases
|
||||||
|
})
|
||||||
|
const app = {}
|
||||||
|
|
||||||
|
restControllers.attachRESTControllers(app)
|
||||||
|
|
||||||
|
assert.isTrue(attachStub.calledOnce)
|
||||||
|
assert.equal(attachStub.getCall(0).args[0], app)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
/*
|
|
||||||
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,137 @@
|
|||||||
|
/*
|
||||||
|
Unit tests for BlockchainUseCases.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
|
||||||
|
import BlockchainUseCases from '../../../src/use-cases/full-node-blockchain-use-cases.js'
|
||||||
|
|
||||||
|
describe('#full-node-blockchain-use-cases.js', () => {
|
||||||
|
let sandbox
|
||||||
|
let mockAdapters
|
||||||
|
let uut
|
||||||
|
|
||||||
|
const createAdapters = () => {
|
||||||
|
return {
|
||||||
|
fullNode: {
|
||||||
|
call: sandbox.stub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox()
|
||||||
|
mockAdapters = createAdapters()
|
||||||
|
uut = new BlockchainUseCases({ adapters: mockAdapters })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#constructor()', () => {
|
||||||
|
it('should require adapters', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new BlockchainUseCases()
|
||||||
|
}, /Adapters instance required/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require full node adapter', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new BlockchainUseCases({ adapters: {} })
|
||||||
|
}, /Full node adapter required/)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getBestBlockHash()', () => {
|
||||||
|
it('should call full node adapter without parameters', async () => {
|
||||||
|
mockAdapters.fullNode.call.resolves('hash')
|
||||||
|
|
||||||
|
const result = await uut.getBestBlockHash()
|
||||||
|
|
||||||
|
assert.equal(result, 'hash')
|
||||||
|
assert.isTrue(mockAdapters.fullNode.call.calledOnceWithExactly('getbestblockhash'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getBlockHeaders()', () => {
|
||||||
|
it('should call adapter for each hash and return aggregated result', async () => {
|
||||||
|
const hashes = ['a'.repeat(64), 'b'.repeat(64)]
|
||||||
|
mockAdapters.fullNode.call
|
||||||
|
.onFirstCall().resolves('header-1')
|
||||||
|
.onSecondCall().resolves('header-2')
|
||||||
|
|
||||||
|
const result = await uut.getBlockHeaders({ hashes, verbose: true })
|
||||||
|
|
||||||
|
assert.deepEqual(result, ['header-1', 'header-2'])
|
||||||
|
assert.isTrue(
|
||||||
|
mockAdapters.fullNode.call.calledWithExactly(
|
||||||
|
'getblockheader',
|
||||||
|
[hashes[0], true],
|
||||||
|
`getblockheader-${hashes[0]}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert.isTrue(
|
||||||
|
mockAdapters.fullNode.call.calledWithExactly(
|
||||||
|
'getblockheader',
|
||||||
|
[hashes[1], true],
|
||||||
|
`getblockheader-${hashes[1]}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should rethrow errors from adapter', async () => {
|
||||||
|
const hashes = ['a'.repeat(64)]
|
||||||
|
mockAdapters.fullNode.call.rejects(new Error('failure'))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await uut.getBlockHeaders({ hashes })
|
||||||
|
assert.fail('Unexpected success')
|
||||||
|
} catch (err) {
|
||||||
|
assert.equal(err.message, 'failure')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#getTxOut()', () => {
|
||||||
|
it('should pass parameters to full node call', async () => {
|
||||||
|
mockAdapters.fullNode.call.resolves({ value: 1 })
|
||||||
|
|
||||||
|
const result = await uut.getTxOut({
|
||||||
|
txid: 'txid',
|
||||||
|
n: 0,
|
||||||
|
includeMempool: true
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.deepEqual(result, { value: 1 })
|
||||||
|
assert.isTrue(
|
||||||
|
mockAdapters.fullNode.call.calledOnceWithExactly(
|
||||||
|
'gettxout',
|
||||||
|
['txid', 0, true]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#verifyTxOutProofs()', () => {
|
||||||
|
it('should call adapter for each proof and return aggregated results', async () => {
|
||||||
|
const proofs = ['proof-1', 'proof-2']
|
||||||
|
mockAdapters.fullNode.call.onFirstCall().resolves(['txid-1'])
|
||||||
|
mockAdapters.fullNode.call.onSecondCall().resolves(['txid-2'])
|
||||||
|
|
||||||
|
const result = await uut.verifyTxOutProofs({ proofs })
|
||||||
|
|
||||||
|
assert.deepEqual(result, [['txid-1'], ['txid-2']])
|
||||||
|
assert.isTrue(
|
||||||
|
mockAdapters.fullNode.call.calledWithExactly(
|
||||||
|
'verifytxoutproof',
|
||||||
|
['proof-1'],
|
||||||
|
`verifytxoutproof-${proofs[0].slice(0, 16)}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
/*
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
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')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
/*
|
|
||||||
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