diff --git a/deno.jsonc b/deno.jsonc index 67bb8da..95668a6 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -21,7 +21,7 @@ "backport": "deno run --no-prompt --allow-read=. --allow-write=. https://deno.land/x/deno2node@v1.14.0/src/cli.ts", "check": "deno lint && deno fmt --check && deno check --allow-import src/mod.ts", "fix": "deno lint --fix && deno fmt", - "test": "deno test --allow-import --seed=123456 --parallel ./test/", + "test": "deno test --allow-import --seed=123456 --parallel ./test/ --hide-stacktraces", "coverage": "rm -rf ./test/cov_profile && deno task test --coverage=./test/cov_profile && deno coverage --lcov --output=./coverage.lcov ./test/cov_profile", "hook": "deno run --allow-read --allow-run --allow-write https://deno.land/x/deno_hooks@0.1.1/mod.ts" }, diff --git a/package.json b/package.json index d492021..6d01f5e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "peerDependencies": { "grammy": "^1.17.1" }, + "dependencies": { + "super-expressive": "^2.0.0" + }, "devDependencies": { "deno2node": "^1.14.0", "typescript": "^5.6.3" diff --git a/src/command-group.ts b/src/command-group.ts index cb7b8eb..1f25c00 100644 --- a/src/command-group.ts +++ b/src/command-group.ts @@ -10,11 +10,8 @@ import { Middleware, } from "./deps.deno.ts"; import type { CommandElementals, CommandOptions } from "./types.ts"; -import { - ensureArray, - getCommandsRegex, - type MaybeArray, -} from "./utils/array.ts"; +import { ensureArray, type MaybeArray } from "./utils/array.ts"; +import { getCommandsLikeRegex } from "./utils/regex.ts"; import { setBotCommands, SetBotCommandsOptions, @@ -417,7 +414,7 @@ function containsCommands< } for (const prefix of allPrefixes) { - const regex = getCommandsRegex(prefix); + const regex = getCommandsLikeRegex(prefix); if (ctx.hasText(regex)) return true; } return false; diff --git a/src/command.ts b/src/command.ts index 19dee50..a5e303a 100644 --- a/src/command.ts +++ b/src/command.ts @@ -22,6 +22,11 @@ import { matchesPattern, } from "./utils/checks.ts"; import { InvalidScopeError } from "./utils/errors.ts"; +import { + DISALLOWED_SPECIAL_CHARACTERS, + escapeEspecial, + NO_PREFIX_COMMAND_MATCHER, +} from "./utils/regex.ts"; type BotCommandGroupsScope = | BotCommandScopeAllGroupChats @@ -47,8 +52,6 @@ export interface CommandMatch { match?: RegExpExecArray | null; } -const NOCASE_COMMAND_NAME_REGEX = /^[0-9a-z_]+$/i; - /** * Class that represents a single command and allows you to configure it. */ @@ -194,11 +197,11 @@ export class Command implements MiddlewareObj { ); } - if (!NOCASE_COMMAND_NAME_REGEX.test(name)) { + if (DISALLOWED_SPECIAL_CHARACTERS.test(name)) { problems.push( - `Command name has special characters (${ - name.replace(/[0-9a-z_]/ig, "") - }). Only letters, digits and _ are allowed`, + `Command name contains the following disallowed special characters: ${ + name.match(DISALLOWED_SPECIAL_CHARACTERS)?.join("") + }\nOnly letters, digits and _ are allowed`, ); } } @@ -371,9 +374,11 @@ export class Command implements MiddlewareObj { } const commandNames = ensureArray(command); - const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + + const escapedPrefix = escapeEspecial(prefix); + const commandRegex = new RegExp( - `${escapedPrefix}(?[^@ ]+)(?:@(?[^\\s]*))?(?.*)`, + escapedPrefix + NO_PREFIX_COMMAND_MATCHER.source, "g", ); diff --git a/src/context.ts b/src/context.ts index b15e72e..f411393 100644 --- a/src/context.ts +++ b/src/context.ts @@ -3,7 +3,8 @@ import { CommandMatch } from "./command.ts"; import { BotCommandScopeChat, Context, NextFunction } from "./deps.deno.ts"; import { SetMyCommandsParams } from "./mod.ts"; import { BotCommandEntity } from "./types.ts"; -import { ensureArray, getCommandsRegex } from "./utils/array.ts"; +import { ensureArray } from "./utils/array.ts"; +import { getCommandsLikeRegex } from "./utils/regex.ts"; import { fuzzyMatch, JaroWinklerOptions } from "./utils/jaro-winkler.ts"; import { setBotCommands, @@ -141,7 +142,7 @@ export function commands() { if (!prefixes.length) return []; const regexes = prefixes.map( - (prefix) => getCommandsRegex(prefix), + (prefix) => getCommandsLikeRegex(prefix), ); const entities = regexes.flatMap((regex) => { let match: RegExpExecArray | null; diff --git a/src/deps.deno.ts b/src/deps.deno.ts index e344dbd..f30e2eb 100644 --- a/src/deps.deno.ts +++ b/src/deps.deno.ts @@ -22,5 +22,7 @@ export type { LanguageCode, MessageEntity, } from "https://lib.deno.dev/x/grammy@1/types.ts"; +import SuperExpressive from "npm:super-expressive"; +export { SuperExpressive }; // TODO: bring this back once the types are available on the "web" runtimes // export { LanguageCodes } from "https://lib.deno.dev/x/grammy@1/types.ts"; diff --git a/src/deps.node.ts b/src/deps.node.ts index cb831b7..93f150c 100644 --- a/src/deps.node.ts +++ b/src/deps.node.ts @@ -22,5 +22,7 @@ export type { LanguageCode, MessageEntity } from "grammy/types"; +import SuperExpressive from "npm:super-expressive"; +export { SuperExpressive }; // TODO: bring this back once the types are available on the "web" runtimes // export { LanguageCodes } from "grammy/types"; \ No newline at end of file diff --git a/src/utils/array.ts b/src/utils/array.ts index e085f61..e31e04e 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -1,21 +1,3 @@ export type MaybeArray = T | T[]; export const ensureArray = (value: MaybeArray): T[] => Array.isArray(value) ? value : [value]; - -const specialChars = "\\.^$|?*+()[]{}-".split(""); - -const replaceAll = (s: string, find: string, replace: string) => - s.replace(new RegExp(`\\${find}`, "g"), replace); - -export function escapeSpecial(str: string) { - return specialChars.reduce( - (acc, char) => replaceAll(acc, char, `\\${char}`), - str, - ); -} -export function getCommandsRegex(prefix: string) { - return new RegExp( - `(\?\<\!\\S)(\?${escapeSpecial(prefix)})\\S+(\\s|$)`, - "g", - ); -} diff --git a/src/utils/regex.ts b/src/utils/regex.ts new file mode 100644 index 0000000..3a9dd3c --- /dev/null +++ b/src/utils/regex.ts @@ -0,0 +1,65 @@ +import { SuperExpressive } from "../deps.deno.ts"; + +export const escapeEspecial = (prefix: string) => + SuperExpressive().namedCapture("prefix") + .string(`${prefix}`) + .end() + .toRegex() + .source; + +const notBehindWhitespace = SuperExpressive() + .assertNotBehind + .nonWhitespaceChar + .end() + .toRegex() + .source; + +const hasCharsAndDetectEnd = SuperExpressive() + .oneOrMore + .nonWhitespaceChar + .anyOf + .whitespaceChar + .endOfInput + .end() + .toRegex() + .source; + +export function getCommandsLikeRegex(prefix: string) { + return new RegExp( + notBehindWhitespace + escapeEspecial(prefix) + hasCharsAndDetectEnd, + "g", + ); +} + +export const DISALLOWED_SPECIAL_CHARACTERS = SuperExpressive() + .caseInsensitive + .allowMultipleMatches + .anythingBut + .range("0", "9") + .range("a", "z") + .char("_") + .end() + .toRegex(); + +export const NO_PREFIX_COMMAND_MATCHER = SuperExpressive() + .namedCapture("command") + .oneOrMore + .anythingButChars("@ ") + .end() + .optional.group + .char("@") + .subexpression( + SuperExpressive() + .namedCapture("username") + .zeroOrMore + .nonWhitespaceChar + .end(), + ) + .end() + .subexpression( + SuperExpressive() + .namedCapture("rest") + .zeroOrMore + .anyChar + .end(), + ).toRegex(); diff --git a/test/command-group.test.ts b/test/command-group.test.ts index b3c12d5..aec61b1 100644 --- a/test/command-group.test.ts +++ b/test/command-group.test.ts @@ -1,6 +1,6 @@ import { CommandGroup } from "../src/command-group.ts"; import { MyCommandParams } from "../src/mod.ts"; -import { dummyCtx } from "./context.test.ts"; +import { getDummyCtx } from "./utils.ts"; import { assert, assertEquals, @@ -54,7 +54,7 @@ describe("CommandGroup", () => { }); describe("setMyCommands", () => { it("should throw if the update has no chat property", () => { - const ctx = dummyCtx({ noMessage: true }); + const ctx = getDummyCtx({ noMessage: true }); const a = new CommandGroup(); assertRejects(() => ctx.setMyCommands(a)); }); @@ -294,7 +294,7 @@ describe("CommandGroup", () => { it("should only consider as entities prefixes registered in the command instance", () => { const text = "/papi hola papacito como estamos /papi /ecco"; - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: text, }); const entities = ctx.getCommandEntities(a); @@ -310,7 +310,7 @@ describe("CommandGroup", () => { } }); it("should get command entities for custom prefixes", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/hi ?momi abcdfghi", }); const entities = ctx.getCommandEntities(a); @@ -339,15 +339,15 @@ describe("CommandGroup", () => { ]); }); it("should throw if you call getCommandEntities on an update with no text", () => { - const ctx = dummyCtx({}); + const ctx = getDummyCtx({}); assertThrows(() => ctx.getCommandEntities([a, b, c])); }); it("should return an empty array if the Commands classes to check against do not have any command register", () => { - const ctx = dummyCtx({ userInput: "/papi" }); + const ctx = getDummyCtx({ userInput: "/papi" }); assertEquals(ctx.getCommandEntities(c), []); }); it("should work across multiple Commands instances", () => { - const ctx = dummyCtx({ userInput: "/papi superprefixmami" }); + const ctx = getDummyCtx({ userInput: "/papi superprefixmami" }); assertEquals( ctx.getCommandEntities([a, b]).map((entity) => entity.prefix), ["/", "superprefix"], diff --git a/test/command.test.ts b/test/command.test.ts index 1f7f23f..b9f005d 100644 --- a/test/command.test.ts +++ b/test/command.test.ts @@ -605,7 +605,7 @@ describe("Command", () => { const command = new Command("*test!", "_"); assertEquals(command.isApiCompliant(), [ false, - "Command name has special characters (*!). Only letters, digits and _ are allowed", + "Command name contains the following disallowed special characters: *!\nOnly letters, digits and _ are allowed", ]); }); @@ -617,7 +617,7 @@ describe("Command", () => { assertEquals(command.isApiCompliant(), [ false, "Command name has uppercase characters", - "Command name has special characters ($). Only letters, digits and _ are allowed", + "Command name contains the following disallowed special characters: $\nOnly letters, digits and _ are allowed", ]); }); }); diff --git a/test/context.test.ts b/test/context.test.ts index 44c93e1..971873f 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -1,25 +1,10 @@ -import { - resolvesNext, - spy, -} from "https://deno.land/std@0.203.0/testing/mock.ts"; -import { commands, type CommandsFlavor } from "../src/mod.ts"; -import { - Api, - assert, - assertRejects, - Chat, - Context, - describe, - it, - Message, - Update, - User, - UserFromGetMe, -} from "./deps.test.ts"; +import { commands } from "../src/mod.ts"; +import { assert, assertRejects, describe, it } from "./deps.test.ts"; +import { getDummyCtx } from "./utils.ts"; describe("commands", () => { it("should install the setMyCommands method on the context", () => { - const context = dummyCtx({}); + const context = getDummyCtx({}); const middleware = commands(); middleware(context, async () => {}); @@ -27,7 +12,7 @@ describe("commands", () => { assert(context.setMyCommands); }); it("should install the getNearestCommand method on the context", () => { - const context = dummyCtx({}); + const context = getDummyCtx({}); const middleware = commands(); middleware(context, async () => {}); @@ -37,7 +22,7 @@ describe("commands", () => { describe("setMyCommands", () => { it("should throw an error if there is no chat", async () => { - const context = dummyCtx({ noMessage: true }); + const context = getDummyCtx({ noMessage: true }); const middleware = commands(); middleware(context, async () => {}); @@ -50,28 +35,3 @@ describe("commands", () => { }); }); }); - -export function dummyCtx({ userInput, language, noMessage }: { - userInput?: string; - language?: string; - noMessage?: boolean; -}) { - const u = { id: 42, first_name: "yo", language_code: language } as User; - const c = { id: 100, type: "private" } as Chat; - const m = noMessage ? undefined : ({ - text: userInput, - from: u, - chat: c, - } as Message); - const update = { - message: m, - } as Update; - const api = { - raw: { setMyCommands: spy(resolvesNext([true] as const)) }, - } as unknown as Api; - const me = { id: 42, username: "bot" } as UserFromGetMe; - const ctx = new Context(update, api, me) as CommandsFlavor; - const middleware = commands(); - middleware(ctx, async () => {}); - return ctx; -} diff --git a/test/integration.test.ts b/test/integration.test.ts index 3a006e0..51331c1 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -3,56 +3,16 @@ import { resolvesNext, } from "https://deno.land/std@0.203.0/testing/mock.ts"; import { CommandGroup } from "../src/command-group.ts"; -import { Bot } from "../src/deps.deno.ts"; -import { Command, commands, CommandsFlavor } from "../src/mod.ts"; +import { Command, commands } from "../src/mod.ts"; import { Api, assertRejects, assertSpyCall, - Chat, - Context, describe, it, - Message, spy, - Update, - User, } from "./deps.test.ts"; - -const getBot = () => - new Bot("dummy_token", { - botInfo: { - id: 1, - is_bot: true, - username: "", - can_join_groups: true, - can_read_all_group_messages: true, - supports_inline_queries: true, - first_name: "", - can_connect_to_business: true, - has_main_web_app: false, - }, - }); - -const getDummyUpdate = ({ userInput, language, noChat, chatType = "private" }: { - userInput?: string; - language?: string; - noChat?: boolean; - chatType?: Chat["type"]; -} = {}) => { - const u = { id: 42, first_name: "yo", language_code: language } as User; - const c = { id: 100, type: chatType } as Chat; - const m = { - text: userInput, - from: u, - chat: noChat ? undefined : c, - } as Message; - const update = { - message: m, - } as Update; - - return update; -}; +import { getBot, getDummyUpdate } from "./utils.ts"; describe("Integration", () => { describe("setCommands", () => { diff --git a/test/jaroWrinkler.test.ts b/test/jaroWrinkler.test.ts index c282724..fc833c5 100644 --- a/test/jaroWrinkler.test.ts +++ b/test/jaroWrinkler.test.ts @@ -4,7 +4,7 @@ import { JaroWinklerDistance, } from "../src/utils/jaro-winkler.ts"; import { CommandGroup } from "../src/mod.ts"; -import { dummyCtx } from "./context.test.ts"; +import { getDummyCtx } from "./utils.ts"; import { assertEquals, assertThrows, @@ -344,13 +344,13 @@ describe("Jaro-Wrinkler Algorithm", () => { cmds.command("entitle", "_", () => {}); it("should throw when no msg is given", () => { - let ctx = dummyCtx({}); + let ctx = getDummyCtx({}); assertThrows(() => ctx.getNearestCommand(cmds)); }); describe("should ignore localization when set to, and search trough all commands", () => { it("ignore even if the language is set", () => { // should this console.warn? or maybe use an overload? - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/duci", language: "es", }); @@ -360,7 +360,7 @@ describe("Jaro-Wrinkler Algorithm", () => { }), "/duc", ); - ctx = dummyCtx({ + ctx = getDummyCtx({ userInput: "/duki", language: "es", }); @@ -372,7 +372,7 @@ describe("Jaro-Wrinkler Algorithm", () => { ); }); it("ignore when the language is not set", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/duki", language: "es", }); @@ -380,7 +380,7 @@ describe("Jaro-Wrinkler Algorithm", () => { ctx.getNearestCommand(cmds, { ignoreLocalization: true }), "/duke", ); - ctx = dummyCtx({ + ctx = getDummyCtx({ userInput: "/macellaoo", language: "es", }); @@ -388,7 +388,7 @@ describe("Jaro-Wrinkler Algorithm", () => { ctx.getNearestCommand(cmds, { ignoreLocalization: true }), "+macellaio", ); - ctx = dummyCtx({ + ctx = getDummyCtx({ userInput: "/dadd", language: "es", }); @@ -396,7 +396,7 @@ describe("Jaro-Wrinkler Algorithm", () => { ctx.getNearestCommand(cmds, { ignoreLocalization: true }), "?daddy", ); - ctx = dummyCtx({ + ctx = getDummyCtx({ userInput: "/duk", language: "es", }); @@ -406,7 +406,7 @@ describe("Jaro-Wrinkler Algorithm", () => { ); }); it("should not restrict itself to default", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/duqu", language: "es", }); @@ -416,7 +416,7 @@ describe("Jaro-Wrinkler Algorithm", () => { ); }); it("language not know, but ignore localization still matches the best similarity", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/duqu", language: "en-papacito", }); @@ -426,7 +426,7 @@ describe("Jaro-Wrinkler Algorithm", () => { ); }); it("should chose localization if not ignore", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/duku", language: "es", }); @@ -434,7 +434,7 @@ describe("Jaro-Wrinkler Algorithm", () => { ctx.getNearestCommand(cmds), "/duque", ); - ctx = dummyCtx({ + ctx = getDummyCtx({ userInput: "/duk", language: "fr", }); @@ -446,12 +446,12 @@ describe("Jaro-Wrinkler Algorithm", () => { }); describe("should not fail even if the language it's not know", () => { it("should fallback to default", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/duko", language: "en-papacito", }); assertEquals(ctx.getNearestCommand(cmds), "/duke"); - ctx = dummyCtx({ + ctx = getDummyCtx({ userInput: "/butxher", language: "no-language", }); @@ -460,21 +460,21 @@ describe("Jaro-Wrinkler Algorithm", () => { }); describe("should work for commands with no localization, even when the language is set", () => { it("ender", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/endr", language: "es", }); assertEquals(ctx.getNearestCommand(cmds), "/ender"); }); it("endanger", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/enanger", language: "en", }); assertEquals(ctx.getNearestCommand(cmds), "/endanger"); }); it("entitle", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/entities", language: "pt", }); @@ -495,19 +495,19 @@ describe("Jaro-Wrinkler Algorithm", () => { .localize("fr", "pere", "_"); it("should get the nearest between multiple command classes", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/papi", language: "es", }); assertEquals(ctx.getNearestCommand([cmds, cmds2]), "/papa"); - ctx = dummyCtx({ + ctx = getDummyCtx({ userInput: "/pai", language: "fr", }); assertEquals(ctx.getNearestCommand([cmds, cmds2]), "/pain"); }); it("Without localization it should get the best between multiple command classes", () => { - let ctx = dummyCtx({ + let ctx = getDummyCtx({ userInput: "/pana", language: "???", }); @@ -517,7 +517,7 @@ describe("Jaro-Wrinkler Algorithm", () => { }), "/pan", ); - ctx = dummyCtx({ + ctx = getDummyCtx({ userInput: "/para", language: "???", }); diff --git a/test/not-found.test.ts b/test/not-found.test.ts index b9bbc94..ff5c72a 100644 --- a/test/not-found.test.ts +++ b/test/not-found.test.ts @@ -1,6 +1,6 @@ import { CommandsFlavor } from "../src/context.ts"; import { CommandGroup, commandNotFound } from "../src/mod.ts"; -import { dummyCtx } from "./context.test.ts"; +import { getDummyCtx } from "./utils.ts"; import { assert, assertEquals, @@ -12,7 +12,7 @@ import { describe("commandNotFound", () => { describe("for inputs containing '/' commands", () => { - const ctx = dummyCtx({ userInput: "/papacin /papazote" }); + const ctx = getDummyCtx({ userInput: "/papacin /papazote" }); it("should return true when no commands are registered", () => { const cmds = new CommandGroup(); const predicate = commandNotFound(cmds); @@ -32,7 +32,7 @@ describe("commandNotFound", () => { }); }); describe("for inputs containing custom prefixed commands", () => { - const ctx = dummyCtx({ userInput: "?papacin +papazote" }); + const ctx = getDummyCtx({ userInput: "?papacin +papazote" }); it("should return false if only '/' commands are registered", () => { const cmds = new CommandGroup(); @@ -72,12 +72,12 @@ describe("commandNotFound", () => { const predicate = commandNotFound(cmds); it("should contain the proper suggestion ", () => { - const ctx = dummyCtx({ userInput: "/papacin" }) as withSuggestion; + const ctx = getDummyCtx({ userInput: "/papacin" }) as withSuggestion; predicate(ctx); assertEquals(ctx.commandSuggestion, "+papacin"); }); it("should be null when the input does not match a suggestion", () => { - const ctx = dummyCtx({ + const ctx = getDummyCtx({ userInput: "/nonadapapi", }) as withSuggestion; predicate(ctx); diff --git a/test/perf.test.ts b/test/perf.test.ts new file mode 100644 index 0000000..128df39 --- /dev/null +++ b/test/perf.test.ts @@ -0,0 +1,53 @@ +import { Command as Command_HEAD, CommandOptions } from "../src/mod.ts"; +import { + Command as Command_MAIN, +} from "https://github.com/grammyjs/commands/raw/main/src/mod.ts"; +import { assert, assertEquals, describe, it } from "./deps.test.ts"; +import { getDummyCtx } from "./utils.ts"; + +describe("Command Matching stress test", () => { + const options: CommandOptions = { + matchOnlyAtStart: true, + prefix: "/", + targetedCommands: "optional", + ignoreCase: false, + }; + + const ctx = getDummyCtx({ userInput: "/start" }); + const m = ctx.message!; + + it("Assert new implamantation is not more than 10% slower", () => { + let iterations = 100000; + const init_HEAD = Date.now(); + while (iterations) { + assertEquals( + Command_HEAD.findMatchingCommand("start", options, ctx), + { + command: "start", + rest: "", + }, + ); + iterations--; + } + const finish_HEAD = Date.now(); + + iterations = 100000; + const init_MAIN = Date.now(); + while (iterations) { + assertEquals( + Command_MAIN.findMatchingCommand("start", options, ctx), + { + command: "start", + rest: "", + }, + ); + iterations--; + } + const finish_MAIN = Date.now(); + + const HEAD_TIME = finish_HEAD - init_HEAD; + const MAIN_TIME = finish_MAIN - init_MAIN; + + assert(HEAD_TIME <= MAIN_TIME + (MAIN_TIME / 100) * 10); + }); +}); diff --git a/test/utils-test.test.ts b/test/utils-test.test.ts index 2c8ff47..20a88ac 100644 --- a/test/utils-test.test.ts +++ b/test/utils-test.test.ts @@ -1,7 +1,7 @@ import { isMiddleware } from "../src/utils/checks.ts"; import { CommandsFlavor } from "../src/context.ts"; import { CommandGroup, commandNotFound } from "../src/mod.ts"; -import { dummyCtx } from "./context.test.ts"; +import { getDummyCtx } from "./utils.ts"; import { assert, assertEquals, diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000..0053588 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,73 @@ +import { resolvesNext } from "https://deno.land/std@0.203.0/testing/mock.ts"; +import { commands, CommandsFlavor } from "../src/context.ts"; +import { Api, Bot, Context } from "../src/deps.deno.ts"; +import { + Chat, + Message, + spy, + Update, + User, + UserFromGetMe, +} from "./deps.test.ts"; + +export const getBot = () => + new Bot("dummy_token", { + botInfo: { + id: 1, + is_bot: true, + username: "", + can_join_groups: true, + can_read_all_group_messages: true, + supports_inline_queries: true, + first_name: "", + can_connect_to_business: true, + has_main_web_app: false, + }, + }); + +export const getDummyUpdate = ( + { userInput, language, noChat, chatType = "private" }: { + userInput?: string; + language?: string; + noChat?: boolean; + chatType?: Chat["type"]; + } = {}, +) => { + const u = { id: 42, first_name: "yo", language_code: language } as User; + const c = { id: 100, type: chatType } as Chat; + const m = { + text: userInput, + from: u, + chat: noChat ? undefined : c, + } as Message; + const update = { + message: m, + } as Update; + + return update; +}; + +export function getDummyCtx({ userInput, language, noMessage }: { + userInput?: string; + language?: string; + noMessage?: boolean; +}) { + const u = { id: 42, first_name: "yo", language_code: language } as User; + const c = { id: 100, type: "private" } as Chat; + const m = noMessage ? undefined : ({ + text: userInput, + from: u, + chat: c, + } as Message); + const update = { + message: m, + } as Update; + const api = { + raw: { setMyCommands: spy(resolvesNext([true] as const)) }, + } as unknown as Api; + const me = { id: 42, username: "bot" } as UserFromGetMe; + const ctx = new Context(update, api, me) as CommandsFlavor; + const middleware = commands(); + middleware(ctx, async () => {}); + return ctx; +}