mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api-base.git
synced 2026-09-21 16:52:00 -07:00
first commit
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
Unit tests for ManageSubscriptionUseCase.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import { mockKind1Event } from '../mocks/event-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import ManageSubscriptionUseCase from '../../../src/use-cases/manage-subscription.js'
|
||||
|
||||
describe('#manage-subscription.js', () => {
|
||||
let sandbox
|
||||
let mockAdapters
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock adapters with multiple relays support
|
||||
const mockRelay1 = {
|
||||
relayUrl: 'wss://relay1.example.com',
|
||||
sendReq: sandbox.stub(),
|
||||
sendClose: sandbox.stub()
|
||||
}
|
||||
const mockRelay2 = {
|
||||
relayUrl: 'wss://relay2.example.com',
|
||||
sendReq: sandbox.stub(),
|
||||
sendClose: sandbox.stub()
|
||||
}
|
||||
|
||||
mockAdapters = {
|
||||
nostrRelays: [mockRelay1, mockRelay2]
|
||||
}
|
||||
|
||||
uut = new ManageSubscriptionUseCase({ adapters: mockAdapters })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#createSubscription()', () => {
|
||||
it('should successfully create a subscription across all relays', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
let onEventCalled = false
|
||||
let onEoseCalled = false
|
||||
let onClosedCalled = false
|
||||
|
||||
const onEvent = (event) => {
|
||||
onEventCalled = true
|
||||
}
|
||||
const onEose = () => {
|
||||
onEoseCalled = true
|
||||
}
|
||||
const onClosed = (message) => {
|
||||
onClosedCalled = true
|
||||
}
|
||||
|
||||
// Mock adapters to resolve
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters, onEvent, onEose, onClosed)
|
||||
|
||||
// Assert adapters were called for both relays
|
||||
assert.isTrue(mockAdapters.nostrRelays[0].sendReq.calledOnce)
|
||||
assert.isTrue(mockAdapters.nostrRelays[1].sendReq.calledOnce)
|
||||
|
||||
// Assert subscription is tracked
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
|
||||
// Test handlers - get them from the subscription info
|
||||
const subscriptionInfo = uut.activeSubscriptions.get(subscriptionId)
|
||||
const handlers = subscriptionInfo.handlers
|
||||
|
||||
// Test event handler (should de-duplicate)
|
||||
handlers.onEvent(mockKind1Event)
|
||||
assert.isTrue(onEventCalled)
|
||||
|
||||
// Simulate EOSE from both relays
|
||||
const relayStatuses = subscriptionInfo.relayStatuses
|
||||
relayStatuses[0].eoseReceived = true
|
||||
relayStatuses[1].eoseReceived = true
|
||||
handlers.onEose()
|
||||
assert.isTrue(onEoseCalled)
|
||||
|
||||
handlers.onClosed('test message')
|
||||
assert.isTrue(onClosedCalled)
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
|
||||
it('should prevent duplicate subscriptions', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
|
||||
try {
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'already exists')
|
||||
}
|
||||
})
|
||||
|
||||
it('should clean up subscription on error', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.rejects(new Error('Connection error'))
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
try {
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
// Should clean up even if some relays fail
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle missing callbacks gracefully', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
await uut.createSubscription(subscriptionId, filters, null, null, null)
|
||||
|
||||
// Should not throw when handlers are null
|
||||
const subscriptionInfo = uut.activeSubscriptions.get(subscriptionId)
|
||||
const handlers = subscriptionInfo.handlers
|
||||
handlers.onEvent(mockKind1Event)
|
||||
handlers.onEose()
|
||||
handlers.onClosed('test')
|
||||
})
|
||||
})
|
||||
|
||||
describe('#closeSubscription()', () => {
|
||||
it('should successfully close a subscription across all relays', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[0].sendClose.resolves()
|
||||
mockAdapters.nostrRelays[1].sendClose.resolves()
|
||||
|
||||
// Create subscription first
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
|
||||
// Close subscription
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
|
||||
// Assert adapters were called for both relays
|
||||
assert.isTrue(mockAdapters.nostrRelays[0].sendClose.calledOnce)
|
||||
assert.isTrue(mockAdapters.nostrRelays[1].sendClose.calledOnce)
|
||||
|
||||
// Assert subscription is removed
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
|
||||
it('should return successfully when closing non-existent subscription (idempotent)', async () => {
|
||||
const subscriptionId = 'non-existent-sub'
|
||||
|
||||
// Should not throw - idempotent operation
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
|
||||
// Should return successfully without error
|
||||
assert.isTrue(true, 'closeSubscription should succeed for non-existent subscription')
|
||||
})
|
||||
|
||||
it('should clean up subscription even on error', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[0].sendClose.rejects(new Error('Close error'))
|
||||
mockAdapters.nostrRelays[1].sendClose.resolves()
|
||||
|
||||
// Create subscription first
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
|
||||
// Close should succeed even if one relay fails
|
||||
await uut.closeSubscription(subscriptionId)
|
||||
|
||||
// Should still clean up
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#hasSubscription()', () => {
|
||||
it('should return false for non-existent subscription', () => {
|
||||
assert.isFalse(uut.hasSubscription('non-existent'))
|
||||
})
|
||||
|
||||
it('should return true for existing subscription', async () => {
|
||||
const subscriptionId = 'test-sub-123'
|
||||
const filters = [{ kinds: [1] }]
|
||||
|
||||
mockAdapters.nostrRelays[0].sendReq.resolves()
|
||||
mockAdapters.nostrRelays[1].sendReq.resolves()
|
||||
|
||||
assert.isFalse(uut.hasSubscription(subscriptionId))
|
||||
await uut.createSubscription(subscriptionId, filters)
|
||||
assert.isTrue(uut.hasSubscription(subscriptionId))
|
||||
})
|
||||
})
|
||||
|
||||
describe('#constructor()', () => {
|
||||
it('should require adapters instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new ManageSubscriptionUseCase()
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Adapters instance required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should require NostrRelay adapters array', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new ManageSubscriptionUseCase({ adapters: {} })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'NostrRelay adapters array required')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
Unit tests for PublishEventUseCase.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import {
|
||||
mockKind1Event,
|
||||
mockInvalidEventMissingId
|
||||
} from '../mocks/event-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import PublishEventUseCase from '../../../src/use-cases/publish-event.js'
|
||||
|
||||
describe('#publish-event.js', () => {
|
||||
let sandbox
|
||||
let mockAdapters
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock adapters with multiple relays support
|
||||
mockAdapters = {
|
||||
nostrRelays: [
|
||||
{ relayUrl: 'wss://relay1.example.com' },
|
||||
{ relayUrl: 'wss://relay2.example.com' }
|
||||
],
|
||||
broadcastEvent: sandbox.stub()
|
||||
}
|
||||
|
||||
uut = new PublishEventUseCase({ adapters: mockAdapters })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#execute()', () => {
|
||||
it('should successfully publish a valid event to all relays', async () => {
|
||||
// Mock broadcast response - at least one relay accepts
|
||||
mockAdapters.broadcastEvent.resolves([
|
||||
{ accepted: true, message: 'event saved', relayUrl: 'wss://relay1.example.com', success: true },
|
||||
{ accepted: true, message: 'event saved', relayUrl: 'wss://relay2.example.com', success: true }
|
||||
])
|
||||
|
||||
const result = await uut.execute(mockKind1Event)
|
||||
|
||||
// Assert adapter was called correctly
|
||||
assert.isTrue(mockAdapters.broadcastEvent.calledOnce)
|
||||
const callArgs = mockAdapters.broadcastEvent.getCall(0).args[0]
|
||||
assert.equal(callArgs.id, mockKind1Event.id)
|
||||
assert.equal(callArgs.kind, mockKind1Event.kind)
|
||||
|
||||
// Assert result
|
||||
assert.property(result, 'accepted')
|
||||
assert.property(result, 'message')
|
||||
assert.property(result, 'eventId')
|
||||
assert.property(result, 'relayResults')
|
||||
assert.property(result, 'acceptedCount')
|
||||
assert.property(result, 'totalRelays')
|
||||
assert.isTrue(result.accepted)
|
||||
assert.equal(result.eventId, mockKind1Event.id)
|
||||
assert.equal(result.acceptedCount, 2)
|
||||
assert.equal(result.totalRelays, 2)
|
||||
})
|
||||
|
||||
it('should handle event rejection from all relays', async () => {
|
||||
// Mock broadcast response - all relays reject
|
||||
mockAdapters.broadcastEvent.resolves([
|
||||
{ accepted: false, message: 'duplicate', relayUrl: 'wss://relay1.example.com', success: true },
|
||||
{ accepted: false, message: 'duplicate', relayUrl: 'wss://relay2.example.com', success: true }
|
||||
])
|
||||
|
||||
const result = await uut.execute(mockKind1Event)
|
||||
|
||||
// Assert result shows rejection
|
||||
assert.isFalse(result.accepted)
|
||||
assert.property(result, 'message')
|
||||
assert.equal(result.eventId, mockKind1Event.id)
|
||||
assert.equal(result.acceptedCount, 0)
|
||||
})
|
||||
|
||||
it('should succeed if at least one relay accepts', async () => {
|
||||
// Mock broadcast response - one accepts, one rejects
|
||||
mockAdapters.broadcastEvent.resolves([
|
||||
{ accepted: true, message: 'event saved', relayUrl: 'wss://relay1.example.com', success: true },
|
||||
{ accepted: false, message: 'duplicate', relayUrl: 'wss://relay2.example.com', success: true }
|
||||
])
|
||||
|
||||
const result = await uut.execute(mockKind1Event)
|
||||
|
||||
// Should succeed if at least one accepts
|
||||
assert.isTrue(result.accepted)
|
||||
assert.equal(result.acceptedCount, 1)
|
||||
assert.equal(result.totalRelays, 2)
|
||||
})
|
||||
|
||||
it('should throw error for invalid event structure', async () => {
|
||||
try {
|
||||
await uut.execute(mockInvalidEventMissingId)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Invalid event structure')
|
||||
assert.isFalse(mockAdapters.broadcastEvent.called)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle adapter errors', async () => {
|
||||
// Mock adapter error
|
||||
const adapterError = new Error('Network error')
|
||||
mockAdapters.broadcastEvent.rejects(adapterError)
|
||||
|
||||
try {
|
||||
await uut.execute(mockKind1Event)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.equal(err.message, 'Network error')
|
||||
assert.isTrue(mockAdapters.broadcastEvent.calledOnce)
|
||||
}
|
||||
})
|
||||
|
||||
it('should require adapters instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new PublishEventUseCase()
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Adapters instance required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should require NostrRelay adapters array', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new PublishEventUseCase({ adapters: {} })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'NostrRelay adapters array required')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Unit tests for QueryEventsUseCase.
|
||||
*/
|
||||
|
||||
// npm libraries
|
||||
import { assert } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
|
||||
// Mocking data libraries
|
||||
import { mockEventsArray } from '../mocks/nostr-relay-mocks.js'
|
||||
|
||||
// Unit under test
|
||||
import QueryEventsUseCase from '../../../src/use-cases/query-events.js'
|
||||
|
||||
describe('#query-events.js', () => {
|
||||
let sandbox
|
||||
let mockAdapters
|
||||
let uut
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.createSandbox()
|
||||
|
||||
// Create mock adapters with multiple relays support
|
||||
mockAdapters = {
|
||||
nostrRelays: [
|
||||
{ relayUrl: 'wss://relay1.example.com' },
|
||||
{ relayUrl: 'wss://relay2.example.com' }
|
||||
],
|
||||
queryAllRelays: sandbox.stub()
|
||||
}
|
||||
|
||||
uut = new QueryEventsUseCase({ adapters: mockAdapters })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
describe('#execute()', () => {
|
||||
it('should successfully query events from all relays and return merged results', async () => {
|
||||
const filters = [{ kinds: [1], limit: 10 }]
|
||||
const subscriptionId = 'test-sub-123'
|
||||
|
||||
// Mock queryAllRelays to return events
|
||||
mockAdapters.queryAllRelays.resolves(mockEventsArray)
|
||||
|
||||
const result = await uut.execute(filters, subscriptionId)
|
||||
|
||||
// Assert adapter was called correctly
|
||||
assert.isTrue(mockAdapters.queryAllRelays.calledOnce)
|
||||
const callArgs = mockAdapters.queryAllRelays.getCall(0).args
|
||||
assert.deepEqual(callArgs[0], filters)
|
||||
assert.equal(callArgs[1], subscriptionId)
|
||||
|
||||
// Assert result contains events
|
||||
assert.isArray(result)
|
||||
assert.equal(result.length, mockEventsArray.length)
|
||||
})
|
||||
|
||||
it('should handle errors from queryAllRelays', async () => {
|
||||
const filters = [{ kinds: [1] }]
|
||||
const subscriptionId = 'test-sub-123'
|
||||
|
||||
mockAdapters.queryAllRelays.rejects(new Error('Query failed'))
|
||||
|
||||
try {
|
||||
await uut.execute(filters, subscriptionId)
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Query failed')
|
||||
}
|
||||
})
|
||||
|
||||
it('should return empty array when no events found', async () => {
|
||||
const filters = [{ kinds: [1] }]
|
||||
const subscriptionId = 'test-sub-123'
|
||||
|
||||
mockAdapters.queryAllRelays.resolves([])
|
||||
|
||||
const result = await uut.execute(filters, subscriptionId)
|
||||
|
||||
assert.isArray(result)
|
||||
assert.equal(result.length, 0)
|
||||
})
|
||||
|
||||
it('should require adapters instance', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new QueryEventsUseCase()
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'Adapters instance required')
|
||||
}
|
||||
})
|
||||
|
||||
it('should require NostrRelay adapters array', () => {
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new QueryEventsUseCase({ adapters: {} })
|
||||
assert.equal(true, false, 'unexpected result')
|
||||
} catch (err) {
|
||||
assert.include(err.message, 'NostrRelay adapters array required')
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user