test(dispatcher): some tests
This commit is contained in:
parent
20fc17bb2e
commit
8965273172
6 changed files with 377 additions and 3 deletions
|
@ -21,6 +21,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mtcute/client": "workspace:^",
|
"@mtcute/client": "workspace:^",
|
||||||
|
"@mtcute/test": "workspace:^",
|
||||||
"events": "3.2.0"
|
"events": "3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
145
packages/dispatcher/src/filters/bots.test.ts
Normal file
145
packages/dispatcher/src/filters/bots.test.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { Message, PeersIndex, tl } from '@mtcute/client'
|
||||||
|
import { createStub, StubTelegramClient } from '@mtcute/test'
|
||||||
|
|
||||||
|
import { MessageContext } from '../index.js'
|
||||||
|
import { command, deeplink } from './bots.js'
|
||||||
|
|
||||||
|
const peers = new PeersIndex()
|
||||||
|
peers.users.set(1, createStub('user', { id: 1 }))
|
||||||
|
peers.chats.set(1, createStub('channel', { id: 1 }))
|
||||||
|
const createMessageContext = (partial: Partial<tl.RawMessage>) =>
|
||||||
|
new MessageContext(
|
||||||
|
StubTelegramClient.full(), // eslint-disable-line
|
||||||
|
new Message(createStub('message', partial), peers, false),
|
||||||
|
)
|
||||||
|
|
||||||
|
describe('filters.command', () => {
|
||||||
|
const getParsedCommand = (text: string, ...params: Parameters<typeof command>) => {
|
||||||
|
const ctx = createMessageContext({
|
||||||
|
message: text,
|
||||||
|
})
|
||||||
|
ctx.client.getAuthState = () => ({
|
||||||
|
isBot: true,
|
||||||
|
userId: 0,
|
||||||
|
selfUsername: 'testbot',
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if (command(...params)(ctx)) return (ctx as any).command
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should only parse given commands', () => {
|
||||||
|
expect(getParsedCommand('/start', 'start')).toEqual(['start'])
|
||||||
|
expect(getParsedCommand('/start', 'stop')).toEqual(null)
|
||||||
|
expect(getParsedCommand('/start', ['start', 'stop'])).toEqual(['start'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should only parse commands to the current bot', () => {
|
||||||
|
expect(getParsedCommand('/start@testbot', 'start')).toEqual(['start'])
|
||||||
|
expect(getParsedCommand('/start@otherbot', 'start')).toEqual(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse command arguments', () => {
|
||||||
|
expect(getParsedCommand('/start foo bar baz', 'start')).toEqual(['start', 'foo', 'bar', 'baz'])
|
||||||
|
expect(getParsedCommand('/start@testbot foo bar baz', 'start')).toEqual(['start', 'foo', 'bar', 'baz'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse quoted command arguments', () => {
|
||||||
|
expect(getParsedCommand('/start foo "bar baz"', 'start')).toEqual(['start', 'foo', 'bar baz'])
|
||||||
|
expect(getParsedCommand('/start foo "bar \\" baz"', 'start')).toEqual(['start', 'foo', 'bar " baz'])
|
||||||
|
expect(getParsedCommand('/start foo "bar \\\\" baz"', 'start')).toEqual(['start', 'foo', 'bar \\" baz'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse custom prefixes', () => {
|
||||||
|
expect(getParsedCommand('!start foo "bar baz"', 'start', { prefixes: '!' })).toEqual([
|
||||||
|
'start',
|
||||||
|
'foo',
|
||||||
|
'bar baz',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be case insensitive by default', () => {
|
||||||
|
expect(getParsedCommand('/START foo', 'start')).toEqual(['start', 'foo'])
|
||||||
|
expect(getParsedCommand('/START foo BAR', 'start')).toEqual(['start', 'foo', 'BAR'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be case sensitive if asked', () => {
|
||||||
|
expect(getParsedCommand('/START foo', 'start', { caseSensitive: true })).toEqual(null)
|
||||||
|
expect(getParsedCommand('/START foo', 'START', { caseSensitive: true })).toEqual(['START', 'foo'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should accept multiple commands to match', () => {
|
||||||
|
expect(getParsedCommand('/foo', ['foo', 'bar'])).toEqual(['foo'])
|
||||||
|
expect(getParsedCommand('/bar', ['foo', 'bar'])).toEqual(['bar'])
|
||||||
|
expect(getParsedCommand('/baz', ['foo', 'bar'])).toEqual(null)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('filters.deeplink', () => {
|
||||||
|
it('should only match given param', () => {
|
||||||
|
const ctx = createMessageContext({
|
||||||
|
message: '/start foo',
|
||||||
|
peerId: { _: 'peerUser', userId: 1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deeplink('bar')(ctx)).toEqual(false)
|
||||||
|
expect(deeplink('foo')(ctx)).toEqual(true)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect((ctx as any).command).toEqual(['start', 'foo'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should add regex matches', () => {
|
||||||
|
const ctx = createMessageContext({
|
||||||
|
message: '/start foo_123',
|
||||||
|
peerId: { _: 'peerUser', userId: 1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deeplink(/^foo_(\d+)$/)(ctx)).toEqual(true)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect((ctx as any).command).toEqual(['start', 'foo_123', '123'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should accept multiple params', () => {
|
||||||
|
const ctx = createMessageContext({
|
||||||
|
message: '/start foo',
|
||||||
|
peerId: { _: 'peerUser', userId: 1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deeplink(['foo', 'bar'])(ctx)).toEqual(true)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect((ctx as any).command).toEqual(['start', 'foo'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should accept multiple regex params', () => {
|
||||||
|
const ctx = createMessageContext({
|
||||||
|
message: '/start foo',
|
||||||
|
peerId: { _: 'peerUser', userId: 1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deeplink([/foo/, /bar/])(ctx)).toEqual(true)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
expect((ctx as any).command).toEqual(['start', 'foo'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail for >1 arguments', () => {
|
||||||
|
const ctx = createMessageContext({
|
||||||
|
message: '/start foo bar',
|
||||||
|
peerId: { _: 'peerUser', userId: 1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deeplink('foo')(ctx)).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail for non-pm messages', () => {
|
||||||
|
const ctx = createMessageContext({
|
||||||
|
message: '/start foo',
|
||||||
|
peerId: { _: 'peerChannel', channelId: 1 },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(deeplink('foo')(ctx)).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
|
@ -23,12 +23,19 @@ import { UpdateFilter } from './types.js'
|
||||||
*/
|
*/
|
||||||
export const command = (
|
export const command = (
|
||||||
commands: MaybeArray<string | RegExp>,
|
commands: MaybeArray<string | RegExp>,
|
||||||
prefixes: MaybeArray<string> | null = '/',
|
{
|
||||||
caseSensitive = false,
|
prefixes = '/',
|
||||||
|
caseSensitive = false,
|
||||||
|
}: {
|
||||||
|
prefixes?: MaybeArray<string> | null
|
||||||
|
caseSensitive?: boolean
|
||||||
|
} = {},
|
||||||
): UpdateFilter<MessageContext, { command: string[] }> => {
|
): UpdateFilter<MessageContext, { command: string[] }> => {
|
||||||
if (!Array.isArray(commands)) commands = [commands]
|
if (!Array.isArray(commands)) commands = [commands]
|
||||||
|
|
||||||
commands = commands.map((i) => (typeof i === 'string' ? i.toLowerCase() : i))
|
if (!caseSensitive) {
|
||||||
|
commands = commands.map((i) => (typeof i === 'string' ? i.toLowerCase() : i))
|
||||||
|
}
|
||||||
|
|
||||||
const argumentsRe = /(["'])(.*?)(?<!\\)\1|(\S+)/g
|
const argumentsRe = /(["'])(.*?)(?<!\\)\1|(\S+)/g
|
||||||
const unescapeRe = /\\(['"])/
|
const unescapeRe = /\\(['"])/
|
||||||
|
@ -67,6 +74,7 @@ export const command = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = m.slice(1, -1)
|
const match = m.slice(1, -1)
|
||||||
|
if (!caseSensitive) match[0] = match[0].toLowerCase()
|
||||||
|
|
||||||
// we use .replace to iterate over global regex, not to replace the text
|
// we use .replace to iterate over global regex, not to replace the text
|
||||||
withoutPrefix.slice(m[0].length).replace(argumentsRe, ($0, $1, $2: string, $3: string) => {
|
withoutPrefix.slice(m[0].length).replace(argumentsRe, ($0, $1, $2: string, $3: string) => {
|
||||||
|
|
195
packages/dispatcher/src/filters/logic.test.ts
Normal file
195
packages/dispatcher/src/filters/logic.test.ts
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { and, not, or } from './logic.js'
|
||||||
|
|
||||||
|
describe('filters.not', () => {
|
||||||
|
it('should negate a given sync filter', () => {
|
||||||
|
const filter = vi.fn().mockReturnValue(true)
|
||||||
|
const negated = not(filter)
|
||||||
|
|
||||||
|
expect(negated(1)).toBe(false)
|
||||||
|
expect(filter).toHaveBeenCalledTimes(1)
|
||||||
|
expect(filter).toHaveBeenCalledWith(1, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should negate a given async filter', async () => {
|
||||||
|
const filter = vi.fn().mockResolvedValue(true)
|
||||||
|
const negated = not(filter)
|
||||||
|
|
||||||
|
await expect(negated(1)).resolves.toBe(false)
|
||||||
|
expect(filter).toHaveBeenCalledTimes(1)
|
||||||
|
expect(filter).toHaveBeenCalledWith(1, undefined)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('filters.and', () => {
|
||||||
|
it.each([
|
||||||
|
['sync', 'sync'],
|
||||||
|
['sync', 'async'],
|
||||||
|
['async', 'sync'],
|
||||||
|
['async', 'async'],
|
||||||
|
])('should combine %s and %s filters', async (aType, bType) => {
|
||||||
|
const a = vi.fn().mockReturnValue(aType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
const b = vi.fn().mockReturnValue(bType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
|
||||||
|
const combined = and(a, b)
|
||||||
|
|
||||||
|
expect(await combined(1)).toBe(true)
|
||||||
|
expect(a).toHaveBeenCalledTimes(1)
|
||||||
|
expect(a).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(b).toHaveBeenCalledTimes(1)
|
||||||
|
expect(b).toHaveBeenCalledWith(1, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['sync', 'sync'],
|
||||||
|
['sync', 'async'],
|
||||||
|
['async', 'sync'],
|
||||||
|
['async', 'async'],
|
||||||
|
])('should not continue execution after false (%s and %s filters)', async (aType, bType) => {
|
||||||
|
const a = vi.fn().mockReturnValue(aType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
const b = vi.fn().mockReturnValue(bType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
|
||||||
|
const combined = and(a, b)
|
||||||
|
|
||||||
|
expect(await combined(1)).toBe(false)
|
||||||
|
expect(a).toHaveBeenCalledTimes(1)
|
||||||
|
expect(a).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(b).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['sync', 'sync', 'sync'],
|
||||||
|
['sync', 'sync', 'async'],
|
||||||
|
['sync', 'async', 'sync'],
|
||||||
|
['sync', 'async', 'async'],
|
||||||
|
['async', 'sync', 'sync'],
|
||||||
|
['async', 'sync', 'async'],
|
||||||
|
['async', 'async', 'sync'],
|
||||||
|
['async', 'async', 'async'],
|
||||||
|
])('should combine %s, %s and %s filters', async (aType, bType) => {
|
||||||
|
const a = vi.fn().mockReturnValue(aType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
const b = vi.fn().mockReturnValue(bType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
const c = vi.fn().mockReturnValue(bType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
|
||||||
|
const combined = and(a, b, c)
|
||||||
|
|
||||||
|
expect(await combined(1)).toBe(true)
|
||||||
|
expect(a).toHaveBeenCalledTimes(1)
|
||||||
|
expect(a).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(b).toHaveBeenCalledTimes(1)
|
||||||
|
expect(b).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(c).toHaveBeenCalledTimes(1)
|
||||||
|
expect(c).toHaveBeenCalledWith(1, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['sync', 'sync', 'sync'],
|
||||||
|
['sync', 'sync', 'async'],
|
||||||
|
['sync', 'async', 'sync'],
|
||||||
|
['sync', 'async', 'async'],
|
||||||
|
['async', 'sync', 'sync'],
|
||||||
|
['async', 'sync', 'async'],
|
||||||
|
['async', 'async', 'sync'],
|
||||||
|
['async', 'async', 'async'],
|
||||||
|
])('should not continue execution after false (%s, %s and %s filters)', async (aType, bType) => {
|
||||||
|
const a = vi.fn().mockReturnValue(aType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
const b = vi.fn().mockReturnValue(bType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
const c = vi.fn().mockReturnValue(bType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
|
||||||
|
const combined = and(a, b, c)
|
||||||
|
|
||||||
|
expect(await combined(1)).toBe(false)
|
||||||
|
expect(a).toHaveBeenCalledTimes(1)
|
||||||
|
expect(a).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(b).toHaveBeenCalledTimes(1)
|
||||||
|
expect(b).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(c).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('filters.or', () => {
|
||||||
|
it.each([
|
||||||
|
['sync', 'sync'],
|
||||||
|
['sync', 'async'],
|
||||||
|
['async', 'sync'],
|
||||||
|
['async', 'async'],
|
||||||
|
])('should combine %s and %s filters', async (aType, bType) => {
|
||||||
|
const a = vi.fn().mockReturnValue(aType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
const b = vi.fn().mockReturnValue(bType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
|
||||||
|
const combined = or(a, b)
|
||||||
|
|
||||||
|
expect(await combined(1)).toBe(false)
|
||||||
|
expect(a).toHaveBeenCalledTimes(1)
|
||||||
|
expect(a).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(b).toHaveBeenCalledTimes(1)
|
||||||
|
expect(b).toHaveBeenCalledWith(1, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['sync', 'sync'],
|
||||||
|
['sync', 'async'],
|
||||||
|
['async', 'sync'],
|
||||||
|
['async', 'async'],
|
||||||
|
])('should not continue execution after true (%s and %s filters)', async (aType, bType) => {
|
||||||
|
const a = vi.fn().mockReturnValue(bType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
const b = vi.fn().mockReturnValue(aType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
|
||||||
|
const combined = or(a, b)
|
||||||
|
|
||||||
|
expect(await combined(1)).toBe(true)
|
||||||
|
expect(a).toHaveBeenCalledTimes(1)
|
||||||
|
expect(a).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(b).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['sync', 'sync', 'sync'],
|
||||||
|
['sync', 'sync', 'async'],
|
||||||
|
['sync', 'async', 'sync'],
|
||||||
|
['sync', 'async', 'async'],
|
||||||
|
['async', 'sync', 'sync'],
|
||||||
|
['async', 'sync', 'async'],
|
||||||
|
['async', 'async', 'sync'],
|
||||||
|
['async', 'async', 'async'],
|
||||||
|
])('should combine %s, %s and %s filters', async (aType, bType) => {
|
||||||
|
const a = vi.fn().mockReturnValue(aType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
const b = vi.fn().mockReturnValue(bType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
const c = vi.fn().mockReturnValue(bType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
|
||||||
|
const combined = or(a, b, c)
|
||||||
|
|
||||||
|
expect(await combined(1)).toBe(false)
|
||||||
|
expect(a).toHaveBeenCalledTimes(1)
|
||||||
|
expect(a).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(b).toHaveBeenCalledTimes(1)
|
||||||
|
expect(b).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(c).toHaveBeenCalledTimes(1)
|
||||||
|
expect(c).toHaveBeenCalledWith(1, undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['sync', 'sync', 'sync'],
|
||||||
|
['sync', 'sync', 'async'],
|
||||||
|
['sync', 'async', 'sync'],
|
||||||
|
['sync', 'async', 'async'],
|
||||||
|
['async', 'sync', 'sync'],
|
||||||
|
['async', 'sync', 'async'],
|
||||||
|
['async', 'async', 'sync'],
|
||||||
|
['async', 'async', 'async'],
|
||||||
|
])('should not continue execution after true (%s, %s and %s filters)', async (aType, bType) => {
|
||||||
|
const a = vi.fn().mockReturnValue(aType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
const b = vi.fn().mockReturnValue(bType === 'sync' ? true : Promise.resolve(true))
|
||||||
|
const c = vi.fn().mockReturnValue(bType === 'sync' ? false : Promise.resolve(false))
|
||||||
|
|
||||||
|
const combined = or(a, b, c)
|
||||||
|
|
||||||
|
expect(await combined(1)).toBe(true)
|
||||||
|
expect(a).toHaveBeenCalledTimes(1)
|
||||||
|
expect(a).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(b).toHaveBeenCalledTimes(1)
|
||||||
|
expect(b).toHaveBeenCalledWith(1, undefined)
|
||||||
|
expect(c).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
|
@ -65,6 +65,28 @@ export class StubTelegramClient extends BaseTelegramClient {
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fake "full" client (i.e. TelegramClient)
|
||||||
|
*
|
||||||
|
* Basically a proxy that returns an empty function for every unknown method
|
||||||
|
*/
|
||||||
|
static full() {
|
||||||
|
const client = new StubTelegramClient()
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||||
|
return new Proxy(client, {
|
||||||
|
get(target, prop) {
|
||||||
|
if (typeof prop === 'string' && !(prop in target)) {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[prop as keyof typeof target]
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
}) as any
|
||||||
|
// i don't want to type this properly since it would require depending test utils on client
|
||||||
|
}
|
||||||
|
|
||||||
// some fake peers handling //
|
// some fake peers handling //
|
||||||
|
|
||||||
readonly _knownChats = new Map<number, tl.TypeChat>()
|
readonly _knownChats = new Map<number, tl.TypeChat>()
|
||||||
|
|
|
@ -184,6 +184,9 @@ importers:
|
||||||
'@mtcute/client':
|
'@mtcute/client':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../client
|
version: link:../client
|
||||||
|
'@mtcute/test':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../test
|
||||||
events:
|
events:
|
||||||
specifier: 3.2.0
|
specifier: 3.2.0
|
||||||
version: 3.2.0
|
version: 3.2.0
|
||||||
|
|
Loading…
Reference in a new issue