first commit

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