Skip to content

Commit fd9699b

Browse files
committed
feat: add schema, factory, handler, and repository tests
1 parent 90b64ab commit fd9699b

4 files changed

Lines changed: 213 additions & 0 deletions

File tree

test/unit/factories/message-handler-factory.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Event } from '../../../src/@types/event'
66
import { EventMessageHandler } from '../../../src/handlers/event-message-handler'
77
import { IWebSocketAdapter } from '../../../src/@types/adapters'
88
import { messageHandlerFactory } from '../../../src/factories/message-handler-factory'
9+
import { CountMessageHandler } from '../../../src/handlers/count-message-handler'
910
import { SubscribeMessageHandler } from '../../../src/handlers/subscribe-message-handler'
1011
import { UnsubscribeMessageHandler } from '../../../src/handlers/unsubscribe-message-handler'
1112
import * as cacheModule from '../../../src/cache/client'
@@ -67,6 +68,12 @@ describe('messageHandlerFactory', () => {
6768
expect(factory([message, adapter])).to.be.an.instanceOf(UnsubscribeMessageHandler)
6869
})
6970

71+
it('returns CountMessageHandler when given a COUNT message', () => {
72+
message = [MessageType.COUNT, 'q1', {}] as any
73+
74+
expect(factory([message, adapter])).to.be.an.instanceOf(CountMessageHandler)
75+
})
76+
7077
it('throws when given an invalid message', () => {
7178
message = [] as any
7279

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import chai from 'chai'
2+
import EventEmitter from 'events'
3+
import sinon from 'sinon'
4+
import sinonChai from 'sinon-chai'
5+
6+
import { IWebSocketAdapter } from '../../../src/@types/adapters'
7+
import { MessageType } from '../../../src/@types/messages'
8+
import { IEventRepository } from '../../../src/@types/repositories'
9+
import { Settings } from '../../../src/@types/settings'
10+
import { WebSocketAdapterEvent } from '../../../src/constants/adapter'
11+
import { CountMessageHandler } from '../../../src/handlers/count-message-handler'
12+
13+
chai.use(sinonChai)
14+
const { expect } = chai
15+
16+
describe('CountMessageHandler', () => {
17+
let webSocket: IWebSocketAdapter
18+
let handler: CountMessageHandler
19+
let eventRepository: IEventRepository
20+
let onMessageStub: sinon.SinonStub
21+
let sandbox: sinon.SinonSandbox
22+
23+
beforeEach(() => {
24+
sandbox = sinon.createSandbox()
25+
eventRepository = {
26+
countByFilters: sandbox.stub().resolves(7),
27+
} as any
28+
29+
webSocket = new EventEmitter() as any
30+
onMessageStub = sandbox.stub()
31+
webSocket.on(WebSocketAdapterEvent.Message, onMessageStub)
32+
33+
handler = new CountMessageHandler(webSocket, eventRepository, () => ({
34+
limits: {
35+
client: {
36+
subscription: {
37+
maxFilters: 10,
38+
maxSubscriptionIdLength: 256,
39+
},
40+
},
41+
},
42+
}) as Settings)
43+
})
44+
45+
afterEach(() => {
46+
webSocket.removeAllListeners()
47+
sandbox.restore()
48+
})
49+
50+
it('emits COUNT message with count on success', async () => {
51+
const message = [MessageType.COUNT, 'q1', {}] as any
52+
53+
await handler.handleMessage(message)
54+
55+
expect(eventRepository.countByFilters).to.have.been.calledOnceWithExactly([{}])
56+
expect(onMessageStub).to.have.been.calledOnceWithExactly([MessageType.COUNT, 'q1', { count: 7 }])
57+
})
58+
59+
it('emits CLOSED message when request is rejected', async () => {
60+
handler = new CountMessageHandler(webSocket, eventRepository, () => ({
61+
limits: {
62+
client: {
63+
subscription: {
64+
maxFilters: 1,
65+
maxSubscriptionIdLength: 256,
66+
},
67+
},
68+
},
69+
}) as Settings)
70+
71+
const message = [MessageType.COUNT, 'q1', { kinds: [1] }, { kinds: [2] }] as any
72+
73+
await handler.handleMessage(message)
74+
75+
expect(eventRepository.countByFilters).to.not.have.been.called
76+
expect(onMessageStub).to.have.been.calledOnce
77+
expect(onMessageStub.firstCall.args[0][0]).to.equal(MessageType.CLOSED)
78+
expect(onMessageStub.firstCall.args[0][1]).to.equal('q1')
79+
})
80+
81+
it('emits CLOSED message when repository fails', async () => {
82+
(eventRepository.countByFilters as sinon.SinonStub).rejects(new Error('boom'))
83+
const message = [MessageType.COUNT, 'q1', {}] as any
84+
85+
await handler.handleMessage(message)
86+
87+
expect(onMessageStub).to.have.been.calledOnceWithExactly([
88+
MessageType.CLOSED,
89+
'q1',
90+
'error: unable to count events',
91+
])
92+
})
93+
})

test/unit/repositories/event-repository.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,71 @@ describe('EventRepository', () => {
417417
})
418418
})
419419

420+
describe('.countByFilters', () => {
421+
it('throws error if filters is empty', async () => {
422+
try {
423+
await repository.countByFilters([])
424+
expect.fail('Expected countByFilters to throw')
425+
} catch (error) {
426+
expect((error as Error).message).to.equal('Filters cannot be empty')
427+
}
428+
})
429+
430+
it('returns count value from query result', async () => {
431+
sandbox.stub(rrDbClient, 'from').returns({
432+
countDistinct: () => ({
433+
first: async () => ({ count: '42' }),
434+
}),
435+
} as any)
436+
437+
const result = await repository.countByFilters([{}])
438+
439+
expect(result).to.equal(42)
440+
})
441+
442+
it('builds union query when there are multiple filters', async () => {
443+
const fromStub = sandbox.stub(rrDbClient, 'from').returns({
444+
countDistinct: () => ({
445+
first: async () => ({ count: '1' }),
446+
}),
447+
} as any)
448+
449+
await repository.countByFilters([{ kinds: [1] }, { authors: ['22e804d26ed16b68db5259e78449e96dab5d464c8f470bda3eb1a70467f2c793'] }])
450+
451+
const sql = fromStub.firstCall.args[0].toString()
452+
expect(sql).to.include(' union ')
453+
})
454+
455+
it('joins tags table for generic tag filters', async () => {
456+
const fromStub = sandbox.stub(rrDbClient, 'from').returns({
457+
countDistinct: () => ({
458+
first: async () => ({ count: '1' }),
459+
}),
460+
} as any)
461+
462+
await repository.countByFilters([{ '#e': ['aaaaaa'] } as any])
463+
464+
const sql = fromStub.firstCall.args[0].toString()
465+
expect(sql).to.include('left join "event_tags"')
466+
expect(sql).to.include('event_tags.tag_name')
467+
})
468+
469+
it('filters out deleted and expired events', async () => {
470+
const fromStub = sandbox.stub(rrDbClient, 'from').returns({
471+
countDistinct: () => ({
472+
first: async () => ({ count: '1' }),
473+
}),
474+
} as any)
475+
476+
await repository.countByFilters([{ kinds: [1] }])
477+
478+
const sql = fromStub.firstCall.args[0].toString()
479+
expect(sql).to.include('"events"."deleted_at" is null')
480+
expect(sql).to.include('"events"."expires_at" is null')
481+
expect(sql).to.include('"events"."expires_at" >')
482+
})
483+
})
484+
420485
describe('.create', () => {
421486
let insertStub: sinon.SinonStub
422487
beforeEach(() => {

test/unit/schemas/message-schema.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,53 @@ describe('NIP-01', () => {
112112
expect(result).to.have.property('error').that.is.not.undefined
113113
})
114114
})
115+
116+
describe('COUNT', () => {
117+
beforeEach(() => {
118+
message = [
119+
'COUNT',
120+
'id',
121+
{
122+
ids: ['aaaa', 'bbbb', 'cccc'],
123+
authors: ['aaaa', 'bbbb', 'cccc'],
124+
kinds: [0, 1, 2, 3],
125+
since: 1000,
126+
until: 1000,
127+
limit: 100,
128+
'#e': ['aa', 'bb', 'cc'],
129+
'#p': ['dd', 'ee', 'ff'],
130+
'#r': ['00', '11', '22'],
131+
},
132+
] as any
133+
})
134+
135+
it('returns same message if valid', () => {
136+
const result = validateSchema(messageSchema)(message)
137+
expect(result.error).to.be.undefined
138+
expect(result).to.have.deep.property('value', message)
139+
})
140+
141+
it('returns error if query ID is missing', () => {
142+
message[1] = null
143+
144+
const result = validateSchema(messageSchema)(message)
145+
expect(result).to.have.property('error').that.is.not.undefined
146+
})
147+
148+
it('returns error if filter is missing', () => {
149+
;(message as any[]).splice(2, 1)
150+
151+
const result = validateSchema(messageSchema)(message)
152+
expect(result).to.have.property('error').that.is.not.undefined
153+
})
154+
155+
it('returns error if there are too many filters', () => {
156+
;(message as any[]).splice(2, 1)
157+
;(message as any[]).push(...range(0, 11).map(() => ({})))
158+
159+
const result = validateSchema(messageSchema)(message)
160+
expect(result).to.have.property('error').that.is.not.undefined
161+
})
162+
})
115163
})
116164
})

0 commit comments

Comments
 (0)