test(dispatcher): some tests

This commit is contained in:
alina 🌸 2023-11-19 04:30:16 +03:00
parent 20fc17bb2e
commit 8965273172
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
6 changed files with 377 additions and 3 deletions

View file

@ -21,6 +21,7 @@
},
"dependencies": {
"@mtcute/client": "workspace:^",
"@mtcute/test": "workspace:^",
"events": "3.2.0"
}
}

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

View file

@ -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) => {

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

View file

@ -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>()

View file

@ -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