mirror of
https://github.com/Cameri/nostream.git
synced 2025-05-04 07:00:16 +02:00
898 lines
27 KiB
TypeScript
898 lines
27 KiB
TypeScript
import EventEmitter from 'events'
|
|
|
|
import Sinon, { SinonFakeTimers, SinonStub } from 'sinon'
|
|
import chai from 'chai'
|
|
import chaiAsPromised from 'chai-as-promised'
|
|
import sinonChai from 'sinon-chai'
|
|
|
|
chai.use(sinonChai)
|
|
chai.use(chaiAsPromised)
|
|
|
|
import { EventLimits, Settings } from '../../../src/@types/settings'
|
|
import { IncomingEventMessage, MessageType } from '../../../src/@types/messages'
|
|
import { Event } from '../../../src/@types/event'
|
|
import { EventKinds } from '../../../src/constants/base'
|
|
import { EventMessageHandler } from '../../../src/handlers/event-message-handler'
|
|
import { IUserRepository } from '../../../src/@types/repositories'
|
|
import { IWebSocketAdapter } from '../../../src/@types/adapters'
|
|
import { WebSocketAdapterEvent } from '../../../src/constants/adapter'
|
|
|
|
const { expect } = chai
|
|
|
|
describe('EventMessageHandler', () => {
|
|
let webSocket: IWebSocketAdapter
|
|
let handler: EventMessageHandler
|
|
let userRepository: IUserRepository
|
|
let event: Event
|
|
let message: IncomingEventMessage
|
|
let sandbox: Sinon.SinonSandbox
|
|
|
|
let originalConsoleWarn: (message?: any, ...optionalParams: any[]) => void | undefined = undefined
|
|
|
|
beforeEach(() => {
|
|
sandbox = Sinon.createSandbox()
|
|
originalConsoleWarn = console.warn
|
|
console.warn = () => undefined
|
|
event = {
|
|
content: 'hello',
|
|
created_at: 1665546189,
|
|
id: 'f'.repeat(64),
|
|
kind: 1,
|
|
pubkey: 'f'.repeat(64),
|
|
sig: 'f'.repeat(128),
|
|
tags: [],
|
|
}
|
|
})
|
|
|
|
afterEach(() => {
|
|
console.warn = originalConsoleWarn
|
|
sandbox.restore()
|
|
})
|
|
|
|
describe('handleMessage', () => {
|
|
let canAcceptEventStub: Sinon.SinonStub
|
|
let isEventValidStub: Sinon.SinonStub
|
|
let strategyFactoryStub: Sinon.SinonStub
|
|
let onMessageSpy: Sinon.SinonSpy
|
|
let strategyExecuteStub: Sinon.SinonStub
|
|
let isRateLimitedStub: Sinon.SinonStub
|
|
let isUserAdmitted: Sinon.SinonStub
|
|
|
|
beforeEach(() => {
|
|
canAcceptEventStub = sandbox.stub(EventMessageHandler.prototype, 'canAcceptEvent' as any)
|
|
isEventValidStub = sandbox.stub(EventMessageHandler.prototype, 'isEventValid' as any)
|
|
isUserAdmitted = sandbox.stub(EventMessageHandler.prototype, 'isUserAdmitted' as any)
|
|
strategyExecuteStub = sandbox.stub()
|
|
strategyFactoryStub = sandbox.stub().returns({
|
|
execute: strategyExecuteStub,
|
|
})
|
|
onMessageSpy = sandbox.fake.returns(undefined)
|
|
webSocket = new EventEmitter() as any
|
|
webSocket.on(WebSocketAdapterEvent.Message, onMessageSpy)
|
|
message = [MessageType.EVENT, event]
|
|
isRateLimitedStub = sandbox.stub(EventMessageHandler.prototype, 'isRateLimited' as any)
|
|
handler = new EventMessageHandler(
|
|
webSocket as any,
|
|
strategyFactoryStub,
|
|
userRepository,
|
|
() => ({}) as any,
|
|
() => ({ hit: async () => false })
|
|
)
|
|
})
|
|
|
|
afterEach(() => {
|
|
isEventValidStub.restore()
|
|
canAcceptEventStub.restore()
|
|
webSocket.removeAllListeners()
|
|
})
|
|
|
|
it('rejects event if it can\'t be accepted', async () => {
|
|
canAcceptEventStub.returns('reason')
|
|
|
|
await handler.handleMessage(message)
|
|
|
|
expect(canAcceptEventStub).to.have.been.calledOnceWithExactly(event)
|
|
expect(onMessageSpy).to.have.been.calledOnceWithExactly(
|
|
[MessageType.OK, event.id, false, 'reason'],
|
|
)
|
|
expect(strategyFactoryStub).not.to.have.been.called
|
|
})
|
|
|
|
it('rejects event if rate-limited', async () => {
|
|
isRateLimitedStub.resolves(true)
|
|
|
|
await handler.handleMessage(message)
|
|
|
|
expect(isRateLimitedStub).to.have.been.calledOnceWithExactly(event)
|
|
expect(onMessageSpy).to.have.been.calledOnceWithExactly(
|
|
[MessageType.OK, event.id, false, 'rate-limited: slow down'],
|
|
)
|
|
expect(strategyFactoryStub).not.to.have.been.called
|
|
})
|
|
|
|
it('rejects event if invalid', async () => {
|
|
isEventValidStub.returns('reason')
|
|
|
|
await handler.handleMessage(message)
|
|
|
|
expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
|
|
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
|
|
expect(strategyFactoryStub).not.to.have.been.called
|
|
})
|
|
|
|
it('rejects event if user is not admitted', async () => {
|
|
isUserAdmitted.resolves('reason')
|
|
|
|
await handler.handleMessage(message)
|
|
|
|
expect(isUserAdmitted).to.have.been.calledWithExactly(event)
|
|
expect(strategyFactoryStub).not.to.have.been.called
|
|
})
|
|
|
|
it('does not call strategy if none given', async () => {
|
|
isEventValidStub.returns(undefined)
|
|
canAcceptEventStub.returns(undefined)
|
|
strategyFactoryStub.returns(undefined)
|
|
|
|
await handler.handleMessage(message)
|
|
|
|
expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
|
|
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
|
|
expect(strategyFactoryStub).to.have.been.calledOnceWithExactly([
|
|
event,
|
|
webSocket,
|
|
])
|
|
expect(strategyExecuteStub).not.to.have.been.called
|
|
})
|
|
|
|
it('calls strategy with event', async () => {
|
|
isEventValidStub.returns(undefined)
|
|
canAcceptEventStub.returns(undefined)
|
|
|
|
await handler.handleMessage(message)
|
|
|
|
expect(isEventValidStub).to.have.been.calledOnceWithExactly(event)
|
|
expect(onMessageSpy).not.to.have.been.calledOnceWithExactly()
|
|
expect(strategyFactoryStub).to.have.been.calledOnceWithExactly([
|
|
event,
|
|
webSocket,
|
|
])
|
|
expect(strategyExecuteStub).to.have.been.calledOnceWithExactly(event)
|
|
})
|
|
|
|
it('does not reject if strategy rejects', async () => {
|
|
isEventValidStub.returns(undefined)
|
|
canAcceptEventStub.returns(undefined)
|
|
|
|
strategyExecuteStub.rejects()
|
|
|
|
return expect(handler.handleMessage(message)).to.eventually.be.fulfilled
|
|
})
|
|
})
|
|
|
|
describe('canAcceptEvent', () => {
|
|
let eventLimits: EventLimits
|
|
let settings: Settings
|
|
let clock: SinonFakeTimers
|
|
|
|
beforeEach(() => {
|
|
clock = Sinon.useFakeTimers(1665546189000)
|
|
|
|
eventLimits = {
|
|
createdAt: {
|
|
maxNegativeDelta: 100000,
|
|
maxPositiveDelta: 100000,
|
|
},
|
|
eventId: {
|
|
minLeadingZeroBits: 0,
|
|
},
|
|
kind: {
|
|
blacklist: [],
|
|
whitelist: [],
|
|
},
|
|
pubkey: {
|
|
minBalance: 0n,
|
|
minLeadingZeroBits: 0,
|
|
blacklist: [],
|
|
whitelist: [],
|
|
},
|
|
content: {
|
|
maxLength: 0,
|
|
},
|
|
}
|
|
settings = {
|
|
limits: {
|
|
event: eventLimits,
|
|
},
|
|
} as any
|
|
handler = new EventMessageHandler(
|
|
{} as any,
|
|
() => null,
|
|
userRepository,
|
|
() => settings,
|
|
() => ({ hit: async () => false })
|
|
)
|
|
})
|
|
|
|
afterEach(() => {
|
|
clock.restore()
|
|
})
|
|
|
|
describe('createdAt', () => {
|
|
describe('maxPositiveDelta', () => {
|
|
it('returns undefined if maxPositiveDelta is zero', () => {
|
|
eventLimits.createdAt.maxPositiveDelta = 0
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if createdDate is too far in the future', () => {
|
|
eventLimits.createdAt.maxPositiveDelta = 100
|
|
event.created_at += 101
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('rejected: created_at is more than 100 seconds in the future')
|
|
})
|
|
})
|
|
|
|
describe('maxNegativeDelta', () => {
|
|
it('returns undefined if maxNegativeDelta is zero', () => {
|
|
eventLimits.createdAt.maxNegativeDelta = 0
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if createdDate is too far in the past', () => {
|
|
eventLimits.createdAt.maxNegativeDelta = 100
|
|
event.created_at -= 101
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('rejected: created_at is more than 100 seconds in the past')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('content', () => {
|
|
describe('maxLength', () => {
|
|
it('returns undefined if maxLength is disabled', () => {
|
|
eventLimits.content = [{ maxLength: 0 }]
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefned if content is not too long', () => {
|
|
eventLimits.content = [{ maxLength: 1 }]
|
|
event.content = 'x'.repeat(1)
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if kind does not match', () => {
|
|
eventLimits.content = [{ kinds: [EventKinds.SET_METADATA], maxLength: 1 }]
|
|
event.content = 'x'
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if kind matches but content is short', () => {
|
|
eventLimits.content = [{ kinds: [EventKinds.TEXT_NOTE], maxLength: 1 }]
|
|
event.content = 'x'
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if kind matches but content is too long', () => {
|
|
eventLimits.content = [{ kinds: [EventKinds.TEXT_NOTE], maxLength: 1 }]
|
|
event.content = 'xx'
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('rejected: content is longer than 1 bytes')
|
|
})
|
|
|
|
it('returns reason if content is too long', () => {
|
|
eventLimits.content = [{ maxLength: 1 }]
|
|
event.content = 'x'.repeat(2)
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('rejected: content is longer than 1 bytes')
|
|
})
|
|
})
|
|
|
|
describe('maxLength (deprecated)', () => {
|
|
it('returns undefined if maxLength is zero', () => {
|
|
eventLimits.content = { maxLength: 0 }
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if content is short', () => {
|
|
eventLimits.content = { maxLength: 100 }
|
|
event.content = 'x'.repeat(100)
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if content is too long', () => {
|
|
eventLimits.content = { maxLength: 1 }
|
|
event.content = 'xx'
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('rejected: content is longer than 1 bytes')
|
|
})
|
|
|
|
it('returns undefined if kind matches and content is short', () => {
|
|
eventLimits.content = { kinds: [EventKinds.TEXT_NOTE], maxLength: 1 }
|
|
event.content = 'x'
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if kind does not match and content is too long', () => {
|
|
eventLimits.content = { kinds: [EventKinds.SET_METADATA], maxLength: 1 }
|
|
event.content = 'xx'
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if content is too long', () => {
|
|
eventLimits.content = { maxLength: 1 }
|
|
event.content = 'xx'
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('rejected: content is longer than 1 bytes')
|
|
})
|
|
|
|
it('returns undefined if content is not set', () => {
|
|
eventLimits.content = undefined
|
|
event.content = 'xx'
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
})
|
|
|
|
describe('maxNegativeDelta', () => {
|
|
it('returns undefined if maxNegativeDelta is zero', () => {
|
|
eventLimits.createdAt.maxNegativeDelta = 0
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if createdDate is too far in the past', () => {
|
|
eventLimits.createdAt.maxNegativeDelta = 100
|
|
event.created_at -= 101
|
|
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('rejected: created_at is more than 100 seconds in the past')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('eventId', () => {
|
|
describe('minLeadingZeroBits', () => {
|
|
it('returns undefined if minLeadingZeroBits is zero', () => {
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if eventId has sufficient proof of work ', () => {
|
|
eventLimits.eventId.minLeadingZeroBits = 15
|
|
event.id = '0001' + 'f'.repeat(60)
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if eventId has insufficient proof of work ', () => {
|
|
eventLimits.eventId.minLeadingZeroBits = 16
|
|
event.id = '00' + 'f'.repeat(62)
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('pow: difficulty 8<16')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('pubkey', () => {
|
|
describe('minLeadingZeroBits', () => {
|
|
it('returns undefined if minLeadingZeroBits is zero', () => {
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if pubkey has sufficient proof of work ', () => {
|
|
eventLimits.pubkey.minLeadingZeroBits = 17
|
|
event.pubkey = '00007' + 'f'.repeat(59)
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if pubkey has insufficient proof of work ', () => {
|
|
eventLimits.pubkey.minLeadingZeroBits = 16
|
|
event.pubkey = '0'.repeat(2) + 'f'.repeat(62)
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('pow: pubkey difficulty 8<16')
|
|
})
|
|
})
|
|
|
|
describe('blacklist', () => {
|
|
it('returns undefined if blacklist is empty', () => {
|
|
eventLimits.pubkey.blacklist = []
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if pubkey is not blacklisted', () => {
|
|
eventLimits.pubkey.blacklist = ['aabbcc']
|
|
event.pubkey = 'fffff'
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if pubkey is not blacklisted by prefix', () => {
|
|
eventLimits.pubkey.blacklist = ['aa55']
|
|
event.pubkey = 'aabbcc'
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if pubkey is blacklisted', () => {
|
|
eventLimits.pubkey.blacklist = ['aabbcc']
|
|
event.pubkey = 'aabbcc'
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: pubkey not allowed')
|
|
})
|
|
|
|
it('returns reason if pubkey is blacklisted by prefix', () => {
|
|
eventLimits.pubkey.blacklist = ['aa55']
|
|
event.pubkey = 'aa55ccddeeff'
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: pubkey not allowed')
|
|
})
|
|
})
|
|
|
|
describe('whitelist', () => {
|
|
it('returns undefined if whitelist is empty', () => {
|
|
eventLimits.pubkey.whitelist = []
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if pubkey is whitelisted', () => {
|
|
eventLimits.pubkey.whitelist = ['aabbcc']
|
|
event.pubkey = 'aabbcc'
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if pubkey is whitelisted by prefix', () => {
|
|
eventLimits.pubkey.whitelist = ['aa55']
|
|
event.pubkey = 'aa55ccddeeff'
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if pubkey is not whitelisted', () => {
|
|
eventLimits.pubkey.whitelist = ['ffffff']
|
|
event.pubkey = 'aabbcc'
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: pubkey not allowed')
|
|
})
|
|
|
|
it('returns reason if pubkey is not whitelisted by prefix', () => {
|
|
eventLimits.pubkey.whitelist = ['aa55']
|
|
event.pubkey = 'aabbccddeeff'
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: pubkey not allowed')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('kind', () => {
|
|
describe('blacklist', () => {
|
|
it('returns undefined if blacklist is empty', () => {
|
|
eventLimits.kind.blacklist = []
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if kind is not blacklisted', () => {
|
|
eventLimits.kind.blacklist = [5]
|
|
event.kind = 4
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if kind is not blacklisted in range', () => {
|
|
eventLimits.kind.blacklist = [[1, 5]]
|
|
event.kind = 6
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if kind is blacklisted in range', () => {
|
|
eventLimits.kind.blacklist = [[1, 5]]
|
|
event.kind = 4
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: event kind 4 not allowed')
|
|
})
|
|
})
|
|
|
|
describe('whitelist', () => {
|
|
it('returns undefined if whitelist is empty', () => {
|
|
eventLimits.kind.whitelist = []
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if kind is whitelisted', () => {
|
|
eventLimits.kind.whitelist = [5]
|
|
event.kind = 5
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns undefined if kind is whitelisted in range', () => {
|
|
eventLimits.kind.whitelist = [[1, 5]]
|
|
event.kind = 3
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.be.undefined
|
|
})
|
|
|
|
it('returns reason if kind is blacklisted and whitelisted in range', () => {
|
|
eventLimits.kind.blacklist = [3]
|
|
eventLimits.kind.whitelist = [[1, 5]]
|
|
event.kind = 3
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: event kind 3 not allowed')
|
|
})
|
|
|
|
it('returns reason if kind is blacklisted and whitelisted', () => {
|
|
eventLimits.kind.blacklist = [3]
|
|
eventLimits.kind.whitelist = [3]
|
|
event.kind = 3
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: event kind 3 not allowed')
|
|
})
|
|
|
|
it('returns reason if kind is not whitelisted', () => {
|
|
eventLimits.kind.whitelist = [5]
|
|
event.kind = 4
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: event kind 4 not allowed')
|
|
})
|
|
|
|
it('returns reason if kind is not whitelisted in range', () => {
|
|
eventLimits.kind.whitelist = [[1, 5]]
|
|
event.kind = 6
|
|
expect(
|
|
(handler as any).canAcceptEvent(event)
|
|
).to.equal('blocked: event kind 6 not allowed')
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('isEventValid', () => {
|
|
beforeEach(() => {
|
|
event = {
|
|
id: 'e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a',
|
|
pubkey: '55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503',
|
|
created_at: 1564498626,
|
|
kind: 0,
|
|
tags: [],
|
|
content: '{"name":"ottman@minds.io","about":"","picture":"https://feat-2311-nostr.minds.io/icon/1002952989368913934/medium/1564498626/1564498626/1653379539"}',
|
|
sig: 'd1de98733de2b412549aa64454722d9b66ab3c68e9e0d0f9c5d42e7bd54c30a06174364b683d2c8dbb386ff47f31e6cb7e2f3c3498d8819ee80421216c8309a9',
|
|
}
|
|
})
|
|
|
|
it('returns undefined if event is valid', () => {
|
|
return expect((handler as any).isEventValid(event)).to.eventually.be.undefined
|
|
})
|
|
|
|
it('returns reason if event id is not valid', () => {
|
|
event.id = 'wrong'
|
|
return expect((handler as any).isEventValid(event)).to.eventually.equal('invalid: event id does not match')
|
|
})
|
|
|
|
it('returns reason if event signature is not valid', () => {
|
|
event.sig = 'wrong'
|
|
return expect((handler as any).isEventValid(event)).to.eventually.equal('invalid: event signature verification failed')
|
|
})
|
|
})
|
|
|
|
describe('isRateLimited', () => {
|
|
let eventLimits: EventLimits
|
|
let settings: Settings
|
|
let rateLimiterHitStub: SinonStub
|
|
let userRepository: IUserRepository
|
|
let getClientAddressStub: Sinon.SinonStub
|
|
let webSocket: IWebSocketAdapter
|
|
|
|
beforeEach(() => {
|
|
eventLimits = {
|
|
rateLimits: [],
|
|
}
|
|
settings = {
|
|
limits: {
|
|
event: eventLimits,
|
|
},
|
|
} as any
|
|
rateLimiterHitStub = sandbox.stub()
|
|
getClientAddressStub = sandbox.stub()
|
|
webSocket = {
|
|
getClientAddress: getClientAddressStub,
|
|
} as any
|
|
handler = new EventMessageHandler(
|
|
webSocket,
|
|
() => null,
|
|
userRepository,
|
|
() => settings,
|
|
() => ({ hit: rateLimiterHitStub })
|
|
)
|
|
})
|
|
|
|
it('fulfills with false if limits setting is not set', async () => {
|
|
settings.limits = undefined
|
|
return expect((handler as any).isRateLimited(event)).to.eventually.be.false
|
|
})
|
|
|
|
|
|
it('fulfills with false if event limits setting is not set', async () => {
|
|
settings.limits.event = undefined
|
|
return expect((handler as any).isRateLimited(event)).to.eventually.be.false
|
|
})
|
|
|
|
it('fulfills with false if rate limits setting is not set', async () => {
|
|
eventLimits.rateLimits = undefined
|
|
return expect((handler as any).isRateLimited(event)).to.eventually.be.false
|
|
})
|
|
|
|
it('fulfills with false if rate limits setting is empty', async () => {
|
|
eventLimits.rateLimits = []
|
|
return expect((handler as any).isRateLimited(event)).to.eventually.be.false
|
|
})
|
|
|
|
it('skips rate limiter if IP is whitelisted', async () => {
|
|
eventLimits.rateLimits = [
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
]
|
|
eventLimits.whitelists = {}
|
|
eventLimits.whitelists.ipAddresses = ['2604:a880:cad:d0::e7e:7001']
|
|
getClientAddressStub.returns('2604:a880:cad:d0::e7e:7001')
|
|
|
|
const actualResult = await (handler as any).isRateLimited(event)
|
|
|
|
expect(actualResult).to.be.false
|
|
expect(rateLimiterHitStub).not.to.have.been.called
|
|
})
|
|
|
|
it('calls rate limiter if IP is not whitelisted', async () => {
|
|
eventLimits.rateLimits = [
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
]
|
|
eventLimits.whitelists = {}
|
|
eventLimits.whitelists.ipAddresses = ['::1']
|
|
getClientAddressStub.returns('2604:a880:cad:d0::e7e:7001')
|
|
|
|
await (handler as any).isRateLimited(event)
|
|
|
|
expect(rateLimiterHitStub).to.have.been.called
|
|
})
|
|
|
|
it('skips rate limiter if pubkey is whitelisted', async () => {
|
|
eventLimits.rateLimits = [
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
]
|
|
eventLimits.whitelists = {}
|
|
eventLimits.whitelists.pubkeys = [event.pubkey]
|
|
|
|
const actualResult = await (handler as any).isRateLimited(event)
|
|
|
|
expect(actualResult).to.be.false
|
|
expect(rateLimiterHitStub).not.to.have.been.called
|
|
})
|
|
|
|
it('calls rate limiter if pubkey is not whitelisted', async () => {
|
|
eventLimits.rateLimits = [
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
]
|
|
eventLimits.whitelists = {}
|
|
eventLimits.whitelists.pubkeys = ['other']
|
|
|
|
await (handler as any).isRateLimited(event)
|
|
|
|
expect(rateLimiterHitStub).to.have.been.called
|
|
})
|
|
|
|
it('calls hit with given rate limit settings', async () => {
|
|
eventLimits.rateLimits = [
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
{
|
|
kinds: [1],
|
|
period: 60000,
|
|
rate: 2,
|
|
},
|
|
{
|
|
kinds: [[0, 3]],
|
|
period: 86400000,
|
|
rate: 3,
|
|
},
|
|
]
|
|
|
|
await (handler as any).isRateLimited(event)
|
|
|
|
expect(rateLimiterHitStub).to.have.been.calledThrice
|
|
expect(rateLimiterHitStub.firstCall).to.have.been.calledWithExactly(
|
|
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:60000',
|
|
1,
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
}
|
|
)
|
|
expect(rateLimiterHitStub.secondCall).to.have.been.calledWithExactly(
|
|
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:60000:[1]',
|
|
1,
|
|
{
|
|
period: 60000,
|
|
rate: 2,
|
|
}
|
|
)
|
|
expect(rateLimiterHitStub.thirdCall).to.have.been.calledWithExactly(
|
|
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:86400000:[[0,3]]',
|
|
1,
|
|
{
|
|
period: 86400000,
|
|
rate: 3,
|
|
}
|
|
)
|
|
})
|
|
|
|
|
|
it('fulfills with false if not rate limited', async () => {
|
|
eventLimits.rateLimits = [
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
{
|
|
kinds: [0],
|
|
period: 60000,
|
|
rate: 2,
|
|
},
|
|
{
|
|
kinds: [[10, 20]],
|
|
period: 86400000,
|
|
rate: 3,
|
|
},
|
|
]
|
|
|
|
rateLimiterHitStub.resolves(false)
|
|
|
|
const actualResult = await (handler as any).isRateLimited(event)
|
|
|
|
expect(rateLimiterHitStub).to.have.been.calledOnceWithExactly(
|
|
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:60000',
|
|
1,
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
)
|
|
expect(actualResult).to.be.false
|
|
})
|
|
|
|
it('fulfills with true if rate limited by second rate limit setting', async () => {
|
|
eventLimits.rateLimits = [
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
{
|
|
kinds: [0],
|
|
period: 60000,
|
|
rate: 2,
|
|
},
|
|
{
|
|
kinds: [[0, 5]],
|
|
period: 180,
|
|
rate: 3,
|
|
},
|
|
]
|
|
|
|
rateLimiterHitStub.onFirstCall().resolves(false)
|
|
rateLimiterHitStub.onSecondCall().resolves(true)
|
|
|
|
const actualResult = await (handler as any).isRateLimited(event)
|
|
|
|
expect(rateLimiterHitStub.firstCall).to.have.been.calledWithExactly(
|
|
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:60000',
|
|
1,
|
|
{
|
|
period: 60000,
|
|
rate: 1,
|
|
},
|
|
)
|
|
expect(rateLimiterHitStub.secondCall).to.have.been.calledWithExactly(
|
|
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:180:[[0,5]]',
|
|
1,
|
|
{
|
|
period: 180,
|
|
rate: 3,
|
|
},
|
|
)
|
|
expect(actualResult).to.be.true
|
|
})
|
|
})
|
|
})
|