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": {
|
||||
"@mtcute/client": "workspace:^",
|
||||
"@mtcute/test": "workspace:^",
|
||||
"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 = (
|
||||
commands: MaybeArray<string | RegExp>,
|
||||
prefixes: MaybeArray<string> | null = '/',
|
||||
caseSensitive = false,
|
||||
{
|
||||
prefixes = '/',
|
||||
caseSensitive = false,
|
||||
}: {
|
||||
prefixes?: MaybeArray<string> | null
|
||||
caseSensitive?: boolean
|
||||
} = {},
|
||||
): UpdateFilter<MessageContext, { command: string[] }> => {
|
||||
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 unescapeRe = /\\(['"])/
|
||||
|
@ -67,6 +74,7 @@ export const command = (
|
|||
}
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 //
|
||||
|
||||
readonly _knownChats = new Map<number, tl.TypeChat>()
|
||||
|
|
|
@ -184,6 +184,9 @@ importers:
|
|||
'@mtcute/client':
|
||||
specifier: workspace:^
|
||||
version: link:../client
|
||||
'@mtcute/test':
|
||||
specifier: workspace:^
|
||||
version: link:../test
|
||||
events:
|
||||
specifier: 3.2.0
|
||||
version: 3.2.0
|
||||
|
|
Loading…
Reference in a new issue