nostream/test/unit/handlers/event-message-handler.spec.ts
Ricardo Arturo Cabral Mejía f9c53eeeb8 feat: massive update
Signed-off-by: Ricardo Arturo Cabral Mejía <me@ricardocabral.io>
2023-02-02 00:19:26 -05:00

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
})
})
})