mirror of
https://github.com/Cameri/nostream.git
synced 2025-03-17 21:31:48 +01:00
test: message/event rate limiting
This commit is contained in:
parent
760cb387bf
commit
b36b62520e
@ -23,13 +23,53 @@
|
||||
"createdAt": {
|
||||
"maxPositiveDelta": 900,
|
||||
"maxNegativeDelta": 0
|
||||
}
|
||||
},
|
||||
"rateLimits": [
|
||||
{
|
||||
"kinds": [[0, 5], 7, [40, 49], [10000, 19999], [30000, 39999]],
|
||||
"period": 60000,
|
||||
"rate": 60
|
||||
},
|
||||
{
|
||||
"kinds": [[20000, 29999]],
|
||||
"period": 60000,
|
||||
"rate": 600
|
||||
},
|
||||
{
|
||||
"period": 3600000,
|
||||
"rate": 3600
|
||||
},
|
||||
{
|
||||
"period": 86400000,
|
||||
"rate": 86400
|
||||
}
|
||||
]
|
||||
},
|
||||
"client": {
|
||||
"subscription": {
|
||||
"maxSubscriptions": 10,
|
||||
"maxFilters": 10
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"rateLimits": [
|
||||
{
|
||||
"period": 60000,
|
||||
"rate": 600
|
||||
},
|
||||
{
|
||||
"period": 3600000,
|
||||
"rate": 3600
|
||||
},
|
||||
{
|
||||
"period": 86400000,
|
||||
"rate": 86400
|
||||
}
|
||||
],
|
||||
"ipWhitelist": [
|
||||
"::1",
|
||||
"::ffff:10.10.10.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -5,4 +5,4 @@ import {
|
||||
RedisScripts,
|
||||
} from 'redis'
|
||||
|
||||
export type Cache = RedisClientType<RedisModules, RedisFunctions, RedisScripts>
|
||||
export type CacheClient = RedisClientType<RedisModules, RedisFunctions, RedisScripts>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Cache } from '../@types/cache'
|
||||
import { CacheClient } from '../@types/cache'
|
||||
import { createLogger } from '../factories/logger-factory'
|
||||
import { ICacheAdapter } from '../@types/adapters'
|
||||
|
||||
@ -7,7 +7,7 @@ const debug = createLogger('redis-adapter')
|
||||
export class RedisAdapter implements ICacheAdapter {
|
||||
private connection: Promise<void>
|
||||
|
||||
public constructor(private readonly client: Cache) {
|
||||
public constructor(private readonly client: CacheClient) {
|
||||
this.connection = client.connect()
|
||||
|
||||
this.connection.catch((error) => this.onClientError(error))
|
||||
|
14
src/cache/client.ts
vendored
14
src/cache/client.ts
vendored
@ -1,15 +1,19 @@
|
||||
import { createClient, RedisClientOptions } from 'redis'
|
||||
import { Cache } from '../@types/cache'
|
||||
import { CacheClient } from '../@types/cache'
|
||||
|
||||
export const getCacheConfig = (): RedisClientOptions => ({
|
||||
url: `redis://${process.env.REDIS_USER}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`,
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
})
|
||||
|
||||
export const getCacheClient = (): Cache => {
|
||||
const config = getCacheConfig()
|
||||
let instance: CacheClient = undefined
|
||||
|
||||
const client = createClient(config)
|
||||
export const getCacheClient = (): CacheClient => {
|
||||
if (!instance) {
|
||||
const config = getCacheConfig()
|
||||
|
||||
return client
|
||||
instance = createClient(config)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
|
@ -6,10 +6,21 @@ export enum EventKinds {
|
||||
ENCRYPTED_DIRECT_MESSAGE = 4,
|
||||
DELETE = 5,
|
||||
REACTION = 7,
|
||||
// Channels
|
||||
CHANNEL_CREATION = 40,
|
||||
CHANNEL_METADATA = 41,
|
||||
CHANNEL_MESSAGE = 42,
|
||||
CHANNEL_HIDE_MESSAGE = 43,
|
||||
CHANNEL_MUTE_USER = 44,
|
||||
CHANNEL_RESERVED_FIRST = 45,
|
||||
CHANNEL_RESERVED_LAST = 49,
|
||||
// Replaceable events
|
||||
REPLACEABLE_FIRST = 10000,
|
||||
REPLACEABLE_LAST = 19999,
|
||||
// Ephemeral events
|
||||
EPHEMERAL_FIRST = 20000,
|
||||
EPHEMERAL_LAST = 29999,
|
||||
// Parameterized replaceable events
|
||||
PARAMETERIZED_REPLACEABLE_FIRST = 30000,
|
||||
PARAMETERIZED_REPLACEABLE_LAST = 39999,
|
||||
}
|
||||
|
@ -129,7 +129,7 @@ export class EventMessageHandler implements IMessageHandler {
|
||||
}
|
||||
|
||||
protected async isRateLimited(event: Event): Promise<boolean> {
|
||||
const rateLimits = this.settings().limits.event?.rateLimits
|
||||
const rateLimits = this.settings().limits?.event?.rateLimits
|
||||
if (!rateLimits || !rateLimits.length) {
|
||||
return
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ export class DeleteEventStrategy implements IEventStrategy<Event, Promise<void>>
|
||||
this.webSocket.emit(WebSocketAdapterEvent.Message, createCommandResult(event.id, true, (count) ? '' : 'duplicate:'))
|
||||
|
||||
const ids = event.tags.reduce(
|
||||
(eventIds, tag) => (tag.length >= 2 && tag[0] === EventTags.Event && tag[1].length === 64)
|
||||
(eventIds, tag) => (tag.length >= 2 && tag[0] === EventTags.Event)
|
||||
? [...eventIds, tag[1]]
|
||||
: eventIds,
|
||||
[] as string[]
|
||||
|
@ -27,6 +27,6 @@ export const createEndOfStoredEventsNoticeMessage = (
|
||||
}
|
||||
|
||||
// NIP-20
|
||||
export const createCommandResult = (eventId: EventId, successful: boolean, message = '') => {
|
||||
export const createCommandResult = (eventId: EventId, successful: boolean, message: string) => {
|
||||
return [MessageType.OK, eventId, successful, message]
|
||||
}
|
||||
|
@ -52,14 +52,20 @@ export class SettingsStatic {
|
||||
},
|
||||
rateLimits: [
|
||||
{
|
||||
kinds: [EventKinds.TEXT_NOTE],
|
||||
kinds: [
|
||||
[EventKinds.SET_METADATA, EventKinds.DELETE],
|
||||
EventKinds.REACTION,
|
||||
[EventKinds.CHANNEL_CREATION, EventKinds.CHANNEL_RESERVED_LAST],
|
||||
[EventKinds.REPLACEABLE_FIRST, EventKinds.REPLACEABLE_LAST],
|
||||
[EventKinds.PARAMETERIZED_REPLACEABLE_FIRST, EventKinds.PARAMETERIZED_REPLACEABLE_LAST],
|
||||
],
|
||||
period: 60000,
|
||||
rate: 60,
|
||||
},
|
||||
{
|
||||
kinds: [[EventKinds.EPHEMERAL_FIRST, EventKinds.EPHEMERAL_LAST]],
|
||||
period: 60000,
|
||||
rate: 240,
|
||||
rate: 600,
|
||||
},
|
||||
{
|
||||
period: 3600000,
|
||||
@ -80,15 +86,15 @@ export class SettingsStatic {
|
||||
message: {
|
||||
rateLimits: [
|
||||
{
|
||||
period: 60000, // minute
|
||||
rate: 240,
|
||||
period: 60000,
|
||||
rate: 600,
|
||||
},
|
||||
{
|
||||
period: 3600000, // hour
|
||||
period: 3600000,
|
||||
rate: 3600,
|
||||
},
|
||||
{
|
||||
period: 86400000, // day
|
||||
period: 86400000,
|
||||
rate: 86400,
|
||||
},
|
||||
],
|
||||
|
@ -223,3 +223,28 @@ export async function waitForNotice(ws: WebSocket): Promise<void> {
|
||||
ws.on('message', onMessage)
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForCommand(ws: WebSocket): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
function cleanup() {
|
||||
ws.removeListener('message', onMessage)
|
||||
ws.removeListener('error', onError)
|
||||
}
|
||||
|
||||
function onError(error: Error) {
|
||||
reject(error)
|
||||
cleanup()
|
||||
}
|
||||
ws.once('error', onError)
|
||||
|
||||
function onMessage(raw: RawData) {
|
||||
const message = JSON.parse(raw.toString('utf8'))
|
||||
if (message[0] === MessageType.OK) {
|
||||
resolve(message)
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
ws.on('message', onMessage)
|
||||
})
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ Feature: NIP-01
|
||||
Scenario: Alice can't post a text_note event with an invalid signature
|
||||
Given someone called Alice
|
||||
When Alice sends a text_note event with invalid signature
|
||||
Then Alice receives a notice with invalid signature
|
||||
Then Alice receives an unsuccessful result
|
||||
|
||||
Scenario: Alice and Bob exchange text_note events
|
||||
Given someone called Alice
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
createEvent,
|
||||
createSubscription,
|
||||
sendEvent,
|
||||
waitForCommand,
|
||||
waitForEOSE,
|
||||
waitForEventCount,
|
||||
waitForNextEvent,
|
||||
@ -237,3 +238,10 @@ Then(/(\w+) receives a notice with (.*)/, async function(name: string, pattern:
|
||||
|
||||
expect(actualNotice).to.contain(pattern)
|
||||
})
|
||||
|
||||
Then(/(\w+) receives an? (\w+) result/, async function(name: string, successful: string) {
|
||||
const ws = this.parameters.clients[name] as WebSocket
|
||||
const command = await waitForCommand(ws)
|
||||
|
||||
expect(command[2]).to.equal(successful === 'successful')
|
||||
})
|
||||
|
@ -12,7 +12,9 @@ import WebSocket from 'ws'
|
||||
|
||||
import { connect, createIdentity, createSubscription } from './helpers'
|
||||
import { AppWorker } from '../../../src/app/worker'
|
||||
import { CacheClient } from '../../../src/@types/cache'
|
||||
import { DatabaseClient } from '../../../src/@types/base'
|
||||
import { getCacheClient } from '../../../src/cache/client'
|
||||
import { getDbClient } from '../../../src/database/client'
|
||||
import { SettingsStatic } from '../../../src/utils/settings'
|
||||
import { workerFactory } from '../../../src/factories/worker-factory'
|
||||
@ -20,9 +22,11 @@ import { workerFactory } from '../../../src/factories/worker-factory'
|
||||
let worker: AppWorker
|
||||
|
||||
let dbClient: DatabaseClient
|
||||
let cacheClient: CacheClient
|
||||
|
||||
BeforeAll({ timeout: 6000 }, async function () {
|
||||
process.env.PORT = '18808'
|
||||
cacheClient = getCacheClient()
|
||||
dbClient = getDbClient()
|
||||
await dbClient.raw('SELECT 1=1')
|
||||
|
||||
@ -35,7 +39,10 @@ BeforeAll({ timeout: 6000 }, async function () {
|
||||
|
||||
AfterAll(async function() {
|
||||
worker.close(async () => {
|
||||
await dbClient.destroy()
|
||||
await Promise.all([
|
||||
cacheClient.disconnect(),
|
||||
dbClient.destroy(),
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -30,6 +30,9 @@ describe('webSocketAdapterFactory', () => {
|
||||
headers: {
|
||||
'sec-websocket-key': Buffer.from('key', 'utf8').toString('base64'),
|
||||
},
|
||||
socket: {
|
||||
remoteAddress: '::1',
|
||||
},
|
||||
} as any
|
||||
const webSocketServerAdapter: IWebSocketServerAdapter = {} as any
|
||||
|
||||
|
@ -50,6 +50,7 @@ describe('DelegatedEventMessageHandler', () => {
|
||||
let strategyFactoryStub: Sinon.SinonStub
|
||||
let onMessageSpy: Sinon.SinonSpy
|
||||
let strategyExecuteStub: Sinon.SinonStub
|
||||
let isRateLimitedStub: Sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
canAcceptEventStub = sandbox.stub(DelegatedEventMessageHandler.prototype, 'canAcceptEvent' as any)
|
||||
@ -62,10 +63,12 @@ describe('DelegatedEventMessageHandler', () => {
|
||||
webSocket = new EventEmitter()
|
||||
webSocket.on(WebSocketAdapterEvent.Message, onMessageSpy)
|
||||
message = [MessageType.EVENT, event]
|
||||
isRateLimitedStub = sandbox.stub(EventMessageHandler.prototype, 'isRateLimited' as any)
|
||||
handler = new DelegatedEventMessageHandler(
|
||||
webSocket as any,
|
||||
strategyFactoryStub,
|
||||
() => ({}) as any,
|
||||
() => ({ hit: async () => false }),
|
||||
)
|
||||
})
|
||||
|
||||
@ -81,7 +84,14 @@ describe('DelegatedEventMessageHandler', () => {
|
||||
await handler.handleMessage(message)
|
||||
|
||||
expect(canAcceptEventStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(onMessageSpy).to.have.been.calledOnceWithExactly(['NOTICE', 'Event rejected: reason'])
|
||||
expect(onMessageSpy).to.have.been.calledOnceWithExactly(
|
||||
[
|
||||
MessageType.OK,
|
||||
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
false,
|
||||
'reason',
|
||||
],
|
||||
)
|
||||
expect(strategyFactoryStub).not.to.have.been.called
|
||||
})
|
||||
|
||||
@ -95,6 +105,18 @@ describe('DelegatedEventMessageHandler', () => {
|
||||
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('does not call strategy if none given', async () => {
|
||||
isEventValidStub.returns(undefined)
|
||||
canAcceptEventStub.returns(undefined)
|
||||
@ -177,7 +199,7 @@ describe('DelegatedEventMessageHandler', () => {
|
||||
parentIsEventValidStub.resolves(undefined)
|
||||
|
||||
event.tags[0][3] = 'wrong sig'
|
||||
return expect((handler as any).isEventValid(event)).to.eventually.equal('Event with id a080fd288b60ac2225ff2e2d815291bd730911e583e177302cc949a15dc2b2dc from 62903b1ff41559daf9ee98ef1ae67cc52f301bb5ce26d14baba3052f649c3f49 is invalid delegated event')
|
||||
return expect((handler as any).isEventValid(event)).to.eventually.equal('invalid: delegation verification failed')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
import EventEmitter from 'events'
|
||||
|
||||
import Sinon, { SinonFakeTimers } from 'sinon'
|
||||
import Sinon, { SinonFakeTimers, SinonStub } from 'sinon'
|
||||
import chai from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
|
||||
@ -49,6 +49,7 @@ describe('EventMessageHandler', () => {
|
||||
let strategyFactoryStub: Sinon.SinonStub
|
||||
let onMessageSpy: Sinon.SinonSpy
|
||||
let strategyExecuteStub: Sinon.SinonStub
|
||||
let isRateLimitedStub: Sinon.SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
canAcceptEventStub = sandbox.stub(EventMessageHandler.prototype, 'canAcceptEvent' as any)
|
||||
@ -61,10 +62,12 @@ describe('EventMessageHandler', () => {
|
||||
webSocket = new EventEmitter()
|
||||
webSocket.on(WebSocketAdapterEvent.Message, onMessageSpy)
|
||||
message = [MessageType.EVENT, event]
|
||||
isRateLimitedStub = sandbox.stub(EventMessageHandler.prototype, 'isRateLimited' as any)
|
||||
handler = new EventMessageHandler(
|
||||
webSocket as any,
|
||||
strategyFactoryStub,
|
||||
() => ({}) as any,
|
||||
() => ({ hit: async () => false })
|
||||
)
|
||||
})
|
||||
|
||||
@ -80,7 +83,21 @@ describe('EventMessageHandler', () => {
|
||||
await handler.handleMessage(message)
|
||||
|
||||
expect(canAcceptEventStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(onMessageSpy).to.have.been.calledOnceWithExactly(['NOTICE', 'Event rejected: reason'])
|
||||
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
|
||||
})
|
||||
|
||||
@ -128,6 +145,7 @@ describe('EventMessageHandler', () => {
|
||||
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
|
||||
@ -169,6 +187,7 @@ describe('EventMessageHandler', () => {
|
||||
{} as any,
|
||||
() => null,
|
||||
() => settings,
|
||||
() => ({ hit: async () => false })
|
||||
)
|
||||
})
|
||||
|
||||
@ -192,7 +211,7 @@ describe('EventMessageHandler', () => {
|
||||
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('created_at is more than 100 seconds in the future')
|
||||
).to.equal('rejected: created_at is more than 100 seconds in the future')
|
||||
})
|
||||
})
|
||||
|
||||
@ -211,7 +230,7 @@ describe('EventMessageHandler', () => {
|
||||
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('created_at is more than 100 seconds in the past')
|
||||
).to.equal('rejected: created_at is more than 100 seconds in the past')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -237,7 +256,7 @@ describe('EventMessageHandler', () => {
|
||||
event.id = '00' + 'f'.repeat(62)
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('insufficient proof of work: event Id has less than 16 leading zero bits')
|
||||
).to.equal('pow: difficulty 8<16')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -263,7 +282,7 @@ describe('EventMessageHandler', () => {
|
||||
event.pubkey = '0'.repeat(2) + 'f'.repeat(62)
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('insufficient proof of work: pubkey has less than 16 leading zero bits')
|
||||
).to.equal('pow: pubkey difficulty 8<16')
|
||||
})
|
||||
})
|
||||
|
||||
@ -296,7 +315,7 @@ describe('EventMessageHandler', () => {
|
||||
event.pubkey = 'aabbcc'
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('pubkey aabbcc is not allowed')
|
||||
).to.equal('blocked: pubkey not allowed')
|
||||
})
|
||||
|
||||
it('returns reason if pubkey is blacklisted by prefix', () => {
|
||||
@ -304,7 +323,7 @@ describe('EventMessageHandler', () => {
|
||||
event.pubkey = 'aa55ccddeeff'
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('pubkey aa55ccddeeff is not allowed')
|
||||
).to.equal('blocked: pubkey not allowed')
|
||||
})
|
||||
})
|
||||
|
||||
@ -337,7 +356,7 @@ describe('EventMessageHandler', () => {
|
||||
event.pubkey = 'aabbcc'
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('pubkey aabbcc is not allowed')
|
||||
).to.equal('blocked: pubkey not allowed')
|
||||
})
|
||||
|
||||
it('returns reason if pubkey is not whitelisted by prefix', () => {
|
||||
@ -345,7 +364,7 @@ describe('EventMessageHandler', () => {
|
||||
event.pubkey = 'aabbccddeeff'
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('pubkey aabbccddeeff is not allowed')
|
||||
).to.equal('blocked: pubkey not allowed')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -380,7 +399,7 @@ describe('EventMessageHandler', () => {
|
||||
event.kind = 4
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('event kind 4 is not allowed')
|
||||
).to.equal('blocked: event kind 4 not allowed')
|
||||
})
|
||||
})
|
||||
|
||||
@ -414,7 +433,7 @@ describe('EventMessageHandler', () => {
|
||||
event.kind = 3
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('event kind 3 is not allowed')
|
||||
).to.equal('blocked: event kind 3 not allowed')
|
||||
})
|
||||
|
||||
it('returns reason if kind is blacklisted and whitelisted', () => {
|
||||
@ -423,7 +442,7 @@ describe('EventMessageHandler', () => {
|
||||
event.kind = 3
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('event kind 3 is not allowed')
|
||||
).to.equal('blocked: event kind 3 not allowed')
|
||||
})
|
||||
|
||||
it('returns reason if kind is not whitelisted', () => {
|
||||
@ -431,7 +450,7 @@ describe('EventMessageHandler', () => {
|
||||
event.kind = 4
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('event kind 4 is not allowed')
|
||||
).to.equal('blocked: event kind 4 not allowed')
|
||||
})
|
||||
|
||||
it('returns reason if kind is not whitelisted in range', () => {
|
||||
@ -439,7 +458,7 @@ describe('EventMessageHandler', () => {
|
||||
event.kind = 6
|
||||
expect(
|
||||
(handler as any).canAcceptEvent(event)
|
||||
).to.equal('event kind 6 is not allowed')
|
||||
).to.equal('blocked: event kind 6 not allowed')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -464,12 +483,148 @@ describe('EventMessageHandler', () => {
|
||||
|
||||
it('returns reason if event id is not valid', () => {
|
||||
event.id = 'wrong'
|
||||
return expect((handler as any).isEventValid(event)).to.eventually.equal('Event with id wrong from 55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503 is not valid')
|
||||
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('Event with id e527fe8b0f64a38c6877f943a9e8841074056ba72aceb31a4c85e6d10b27095a from 55b702c167c85eb1c2d5ab35d68bedd1a35b94c01147364d2395c2f66f35a503 has invalid signature')
|
||||
return expect((handler as any).isEventValid(event)).to.eventually.equal('invalid: event signature verification failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isRateLimited', () => {
|
||||
let eventLimits: EventLimits
|
||||
let settings: ISettings
|
||||
let rateLimiterHitStub: SinonStub
|
||||
|
||||
beforeEach(() => {
|
||||
eventLimits = {
|
||||
rateLimits: [],
|
||||
}
|
||||
settings = {
|
||||
limits: {
|
||||
event: eventLimits,
|
||||
},
|
||||
} as any
|
||||
rateLimiterHitStub = sandbox.stub()
|
||||
handler = new EventMessageHandler(
|
||||
{} as any,
|
||||
() => null,
|
||||
() => settings,
|
||||
() => ({ hit: rateLimiterHitStub })
|
||||
)
|
||||
})
|
||||
|
||||
it('returns undefined if rate limits setting is not set', async () => {
|
||||
eventLimits.rateLimits = undefined
|
||||
return expect((handler as any).isRateLimited(event)).to.eventually.be.undefined
|
||||
})
|
||||
|
||||
it('returns undefined if rate limits setting is empty', async () => {
|
||||
eventLimits.rateLimits = []
|
||||
return expect((handler as any).isRateLimited(event)).to.eventually.be.undefined
|
||||
})
|
||||
|
||||
it('calls hit with given rate limit settings', async () => {
|
||||
eventLimits.rateLimits = [
|
||||
{
|
||||
period: 60000,
|
||||
rate: 1,
|
||||
},
|
||||
{
|
||||
kinds: [0],
|
||||
period: 60000,
|
||||
rate: 2,
|
||||
},
|
||||
{
|
||||
kinds: [[10, 20]],
|
||||
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:[0]',
|
||||
1,
|
||||
{
|
||||
period: 60000,
|
||||
rate: 2,
|
||||
}
|
||||
)
|
||||
expect(rateLimiterHitStub.thirdCall).to.have.been.calledWithExactly(
|
||||
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff:events:86400000:[[10,20]]',
|
||||
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.calledThrice
|
||||
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: [[10, 20]],
|
||||
period: 86400000,
|
||||
rate: 3,
|
||||
},
|
||||
]
|
||||
|
||||
rateLimiterHitStub.onFirstCall().resolves(false)
|
||||
rateLimiterHitStub.onSecondCall().resolves(true)
|
||||
rateLimiterHitStub.onThirdCall().resolves(false)
|
||||
|
||||
const actualResult = await (handler as any).isRateLimited(event)
|
||||
|
||||
expect(rateLimiterHitStub).to.have.been.calledThrice
|
||||
expect(actualResult).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -11,12 +11,15 @@ import { EventRepository } from '../../../../src/repositories/event-repository'
|
||||
import { IEventRepository } from '../../../../src/@types/repositories'
|
||||
import { IEventStrategy } from '../../../../src/@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../../../src/@types/adapters'
|
||||
import { MessageType } from '../../../../src/@types/messages'
|
||||
import { WebSocketAdapterEvent } from '../../../../src/constants/adapter'
|
||||
|
||||
const { expect } = chai
|
||||
|
||||
describe('DefaultEventStrategy', () => {
|
||||
const event: Event = {} as any
|
||||
const event: Event = {
|
||||
id: 'id',
|
||||
} as any
|
||||
let webSocket: IWebSocketAdapter
|
||||
let eventRepository: IEventRepository
|
||||
|
||||
@ -59,12 +62,29 @@ describe('DefaultEventStrategy', () => {
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
expect(webSocketEmitStub).to.have.been.calledTwice
|
||||
expect(webSocketEmitStub).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
[MessageType.OK, 'id', true, '']
|
||||
)
|
||||
expect(webSocketEmitStub).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Broadcast,
|
||||
event
|
||||
)
|
||||
})
|
||||
|
||||
it('does not broadcast event if event is duplicate', async () => {
|
||||
eventRepositoryCreateStub.resolves(0)
|
||||
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
['OK', 'id', true, 'duplicate:']
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects if unable to create event', async () => {
|
||||
const error = new Error()
|
||||
eventRepositoryCreateStub.rejects(error)
|
||||
|
@ -12,12 +12,14 @@ import { EventTags } from '../../../../src/constants/base'
|
||||
import { IEventRepository } from '../../../../src/@types/repositories'
|
||||
import { IEventStrategy } from '../../../../src/@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../../../src/@types/adapters'
|
||||
import { MessageType } from '../../../../src/@types/messages'
|
||||
import { WebSocketAdapterEvent } from '../../../../src/constants/adapter'
|
||||
|
||||
const { expect } = chai
|
||||
|
||||
describe('DeleteEventStrategy', () => {
|
||||
const event: Event = {
|
||||
id: 'id',
|
||||
pubkey: 'pubkey',
|
||||
tags: [
|
||||
[EventTags.Event, 'event id 1'],
|
||||
@ -79,18 +81,34 @@ describe('DeleteEventStrategy', () => {
|
||||
expect(eventRepositoryDeleteByPubkeyAndIdsStub).not.to.have.been.called
|
||||
})
|
||||
|
||||
it('broadcast event', async () => {
|
||||
eventRepositoryCreateStub.resolves()
|
||||
it('broadcast event if created', async () => {
|
||||
eventRepositoryCreateStub.resolves(1)
|
||||
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryCreateStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
expect(webSocketEmitStub).to.have.been.calledTwice
|
||||
expect(webSocketEmitStub).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
[MessageType.OK, 'id', true, '']
|
||||
)
|
||||
expect(webSocketEmitStub).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Broadcast,
|
||||
event
|
||||
)
|
||||
})
|
||||
|
||||
it('does not broadcast event if duplicate', async () => {
|
||||
eventRepositoryCreateStub.resolves(0)
|
||||
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
[MessageType.OK, 'id', true, 'duplicate:']
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects if unable to create event', async () => {
|
||||
const error = new Error()
|
||||
eventRepositoryCreateStub.rejects(error)
|
||||
|
@ -11,6 +11,7 @@ import { EventRepository } from '../../../../src/repositories/event-repository'
|
||||
import { IEventRepository } from '../../../../src/@types/repositories'
|
||||
import { IEventStrategy } from '../../../../src/@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../../../src/@types/adapters'
|
||||
import { MessageType } from '../../../../src/@types/messages'
|
||||
import { ParameterizedReplaceableEventStrategy } from '../../../../src/handlers/event-strategies/parameterized-replaceable-event-strategy'
|
||||
import { WebSocketAdapterEvent } from '../../../../src/constants/adapter'
|
||||
|
||||
@ -18,6 +19,7 @@ const { expect } = chai
|
||||
|
||||
describe('ParameterizedReplaceableEventStrategy', () => {
|
||||
const event: Event = {
|
||||
id: 'id',
|
||||
tags: [
|
||||
[EventTags.Deduplication, 'dedup'],
|
||||
],
|
||||
@ -82,12 +84,28 @@ describe('ParameterizedReplaceableEventStrategy', () => {
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryUpsertStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
expect(webSocketEmitStub).to.have.been.calledTwice
|
||||
expect(webSocketEmitStub).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
[MessageType.OK, 'id', true, '']
|
||||
)
|
||||
expect(webSocketEmitStub).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Broadcast,
|
||||
event
|
||||
)
|
||||
})
|
||||
|
||||
it('does not broadcast event if event is duplicate', async () => {
|
||||
eventRepositoryUpsertStub.resolves(0)
|
||||
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
[MessageType.OK, 'id', true, 'duplicate:']
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects if unable to upsert event', async () => {
|
||||
const error = new Error()
|
||||
eventRepositoryUpsertStub.rejects(error)
|
||||
|
@ -10,13 +10,16 @@ import { EventRepository } from '../../../../src/repositories/event-repository'
|
||||
import { IEventRepository } from '../../../../src/@types/repositories'
|
||||
import { IEventStrategy } from '../../../../src/@types/message-handlers'
|
||||
import { IWebSocketAdapter } from '../../../../src/@types/adapters'
|
||||
import { MessageType } from '../../../../src/@types/messages'
|
||||
import { ReplaceableEventStrategy } from '../../../../src/handlers/event-strategies/replaceable-event-strategy'
|
||||
import { WebSocketAdapterEvent } from '../../../../src/constants/adapter'
|
||||
|
||||
const { expect } = chai
|
||||
|
||||
describe('ReplaceableEventStrategy', () => {
|
||||
const event: Event = {} as any
|
||||
const event: Event = {
|
||||
id: 'id',
|
||||
} as any
|
||||
let webSocket: IWebSocketAdapter
|
||||
let eventRepository: IEventRepository
|
||||
|
||||
@ -59,12 +62,29 @@ describe('ReplaceableEventStrategy', () => {
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryUpsertStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
expect(webSocketEmitStub).to.have.been.calledTwice
|
||||
expect(webSocketEmitStub).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
[MessageType.OK, 'id', true, '']
|
||||
)
|
||||
expect(webSocketEmitStub).to.have.been.calledWithExactly(
|
||||
WebSocketAdapterEvent.Broadcast,
|
||||
event
|
||||
)
|
||||
})
|
||||
|
||||
it('does not broadcast event if event is duplicate', async () => {
|
||||
eventRepositoryUpsertStub.resolves(0)
|
||||
|
||||
await strategy.execute(event)
|
||||
|
||||
expect(eventRepositoryUpsertStub).to.have.been.calledOnceWithExactly(event)
|
||||
expect(webSocketEmitStub).to.have.been.calledOnceWithExactly(
|
||||
WebSocketAdapterEvent.Message,
|
||||
['OK', 'id', true, 'duplicate:']
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects if unable to upsert event', async () => {
|
||||
const error = new Error()
|
||||
eventRepositoryUpsertStub.rejects(error)
|
||||
|
@ -72,6 +72,26 @@ describe('SettingsStatic', () => {
|
||||
maxPositiveDelta: 900, // +15 min
|
||||
maxNegativeDelta: 0, // disabled
|
||||
},
|
||||
'rateLimits': [
|
||||
{
|
||||
'kinds': [[0, 5], 7, [40, 49], [10000, 19999], [30000, 39999]],
|
||||
'period': 60000,
|
||||
'rate': 60,
|
||||
},
|
||||
{
|
||||
'kinds': [[20000, 29999]],
|
||||
'period': 60000,
|
||||
'rate': 600,
|
||||
},
|
||||
{
|
||||
'period': 3600000,
|
||||
'rate': 3600,
|
||||
},
|
||||
{
|
||||
'period': 86400000,
|
||||
'rate': 86400,
|
||||
},
|
||||
],
|
||||
},
|
||||
client: {
|
||||
subscription: {
|
||||
@ -80,9 +100,20 @@ describe('SettingsStatic', () => {
|
||||
},
|
||||
},
|
||||
message: {
|
||||
dailyRate: 86400,
|
||||
hourlyRate: 3600,
|
||||
minutelyRate: 240,
|
||||
'rateLimits': [
|
||||
{
|
||||
'period': 60000,
|
||||
'rate': 600,
|
||||
},
|
||||
{
|
||||
'period': 3600000,
|
||||
'rate': 3600,
|
||||
},
|
||||
{
|
||||
'period': 86400000,
|
||||
'rate': 86400,
|
||||
},
|
||||
],
|
||||
ipWhitelist: [
|
||||
'::1',
|
||||
'::ffff:10.10.10.1',
|
||||
|
64
test/unit/utils/sliding-window-rate-limiter.spec.ts
Normal file
64
test/unit/utils/sliding-window-rate-limiter.spec.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { expect } from 'chai'
|
||||
import Sinon from 'sinon'
|
||||
|
||||
import { ICacheAdapter } from '../../../src/@types/adapters'
|
||||
import { IRateLimiter } from '../../../src/@types/utils'
|
||||
import { SlidingWindowRateLimiter } from '../../../src/utils/sliding-window-rate-limiter'
|
||||
|
||||
describe('SlidingWindowRateLimiter', () => {
|
||||
let clock: Sinon.SinonFakeTimers
|
||||
let cache: ICacheAdapter
|
||||
let rateLimiter: IRateLimiter
|
||||
|
||||
let removeRangeByScoreFromSortedSetStub: Sinon.SinonStub
|
||||
let addToSortedSetStub: Sinon.SinonStub
|
||||
let getRangeFromSortedSetStub: Sinon.SinonStub
|
||||
let setKeyExpiryStub: Sinon.SinonStub
|
||||
|
||||
let sandbox: Sinon.SinonSandbox
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox = Sinon.createSandbox()
|
||||
clock = sandbox.useFakeTimers(1665546189000)
|
||||
removeRangeByScoreFromSortedSetStub = sandbox.stub()
|
||||
addToSortedSetStub = sandbox.stub()
|
||||
getRangeFromSortedSetStub = sandbox.stub()
|
||||
setKeyExpiryStub = sandbox.stub()
|
||||
cache = {
|
||||
removeRangeByScoreFromSortedSet: removeRangeByScoreFromSortedSetStub,
|
||||
addToSortedSet: addToSortedSetStub,
|
||||
getRangeFromSortedSet: getRangeFromSortedSetStub,
|
||||
setKeyExpiry: setKeyExpiryStub,
|
||||
}
|
||||
rateLimiter = new SlidingWindowRateLimiter(cache)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
clock.restore()
|
||||
sandbox.restore()
|
||||
})
|
||||
|
||||
it('returns true if rate limited', async () => {
|
||||
const now = Date.now()
|
||||
getRangeFromSortedSetStub.resolves([
|
||||
`${now}:6`,
|
||||
`${now}:4`,
|
||||
`${now}:1`,
|
||||
])
|
||||
|
||||
const actualResult = await rateLimiter.hit('key', 1, { period: 60000, rate: 10 })
|
||||
|
||||
expect(actualResult).to.be.true
|
||||
})
|
||||
|
||||
it('returns false if not rate limited',async () => {
|
||||
const now = Date.now()
|
||||
getRangeFromSortedSetStub.resolves([
|
||||
`${now}:10`,
|
||||
])
|
||||
|
||||
const actualResult = await rateLimiter.hit('key', 1, { period: 60000, rate: 10 })
|
||||
|
||||
expect(actualResult).to.be.false
|
||||
})
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user