@yaebal/test 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/lib/index.d.ts +56 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +119 -0
- package/lib/index.js.map +1 -0
- package/lib/index.test.d.ts +2 -0
- package/lib/index.test.d.ts.map +1 -0
- package/lib/index.test.js +66 -0
- package/lib/index.test.js.map +1 -0
- package/package.json +49 -0
- package/src/index.test.ts +101 -0
- package/src/index.ts +173 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 neverlane
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @yaebal/test
|
|
2
|
+
|
|
3
|
+
testing utilities for yaebal bots: a fake `Api` that records every call, update factories, and helpers to run middleware in isolation.
|
|
4
|
+
|
|
5
|
+
## install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add -D @yaebal/test
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { callbackUpdate, createContext, messageUpdate, mockApi, runMiddleware } from "@yaebal/test";
|
|
15
|
+
import { expect, test } from "vitest";
|
|
16
|
+
|
|
17
|
+
test("replies to /start", async () => {
|
|
18
|
+
const { api, calls } = mockApi();
|
|
19
|
+
const update = messageUpdate({ text: "/start", chatId: 42 });
|
|
20
|
+
const ctx = createContext(update, api);
|
|
21
|
+
|
|
22
|
+
await runMiddleware(bot, ctx);
|
|
23
|
+
|
|
24
|
+
expect(calls[0]?.method).toBe("sendMessage");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("callback query", async () => {
|
|
28
|
+
const { api, calls } = mockApi();
|
|
29
|
+
const update = callbackUpdate({ data: "vote:up", chatId: 1 });
|
|
30
|
+
const ctx = createContext(update, api);
|
|
31
|
+
|
|
32
|
+
await runMiddleware(bot, ctx);
|
|
33
|
+
|
|
34
|
+
expect(calls[0]?.method).toBe("answerCallbackQuery");
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
part of [**yaebal**](https://github.com/neverlane/yaebal) — a type-safe, runtime-agnostic Telegram Bot API framework. MIT.
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @yaebal/test — testing utilities for yaebal bots.
|
|
3
|
+
*
|
|
4
|
+
* every plugin test used to hand-build fake updates and a mock api. this package
|
|
5
|
+
* extracts that boilerplate: {@link mockApi} records every call, the `*Update`
|
|
6
|
+
* factories produce real {@link Update} shapes, and {@link createContext} wraps
|
|
7
|
+
* one in a core {@link Context}.
|
|
8
|
+
*/
|
|
9
|
+
import { type Api, type Composer, Context, type Message, type Update, type UpdateName } from "@yaebal/core";
|
|
10
|
+
/** a single recorded api call: the method name and the params it was given. */
|
|
11
|
+
export interface RecordedCall {
|
|
12
|
+
method: string;
|
|
13
|
+
params: Record<string, unknown> | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** result of {@link mockApi}: the fake `api` plus the array it records into. */
|
|
16
|
+
export interface MockApi {
|
|
17
|
+
api: Api;
|
|
18
|
+
calls: RecordedCall[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* a fake {@link Api} whose every method records `{ method, params }` into `calls`
|
|
22
|
+
* and resolves to a sensible default (`{ message_id: 1 }` for `send*`, `true` for
|
|
23
|
+
* `answerCallbackQuery`, `{}` otherwise). hook registrars (`before`/`after`/
|
|
24
|
+
* `onError`) are no-ops that return the api for chaining.
|
|
25
|
+
*/
|
|
26
|
+
export declare function mockApi(): MockApi;
|
|
27
|
+
/** build an {@link Update} from a partial, filling in a fresh `update_id`. */
|
|
28
|
+
export declare function createUpdate(partial?: Partial<Update>): Update;
|
|
29
|
+
/** options for {@link messageUpdate}. */
|
|
30
|
+
export interface MessageUpdateOptions {
|
|
31
|
+
text?: string;
|
|
32
|
+
chatId?: number;
|
|
33
|
+
fromId?: number;
|
|
34
|
+
chatType?: "private" | "group" | "supergroup" | "channel";
|
|
35
|
+
}
|
|
36
|
+
/** build a message {@link Update}. */
|
|
37
|
+
export declare function messageUpdate(options?: MessageUpdateOptions): Update;
|
|
38
|
+
/** options for {@link callbackUpdate}. */
|
|
39
|
+
export interface CallbackUpdateOptions {
|
|
40
|
+
data?: string;
|
|
41
|
+
chatId?: number;
|
|
42
|
+
fromId?: number;
|
|
43
|
+
}
|
|
44
|
+
/** build a callback_query {@link Update}. */
|
|
45
|
+
export declare function callbackUpdate(options?: CallbackUpdateOptions): Update;
|
|
46
|
+
/** infer which payload key an update carries; defaults to `"message"`. */
|
|
47
|
+
export declare function detectUpdateType(update: Update): UpdateName;
|
|
48
|
+
/**
|
|
49
|
+
* wrap an {@link Update} in a core {@link Context}. the api defaults to a fresh
|
|
50
|
+
* {@link mockApi}; pass `updateType` to override the auto-detected one.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createContext(update: Update, api?: Api, updateType?: UpdateName): Context;
|
|
53
|
+
/** run a composer's middleware against a context. resolves when the chain settles. */
|
|
54
|
+
export declare function runMiddleware<C extends Context>(composer: Composer<C>, ctx: C): Promise<void>;
|
|
55
|
+
export type { Message };
|
|
56
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACN,KAAK,GAAG,EACR,KAAK,QAAQ,EACb,OAAO,EACP,KAAK,OAAO,EAEZ,KAAK,MAAM,EACX,KAAK,UAAU,EACf,MAAM,cAAc,CAAC;AAEtB,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CAC5C;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,KAAK,EAAE,YAAY,EAAE,CAAC;CACtB;AAcD;;;;;GAKG;AACH,wBAAgB,OAAO,IAAI,OAAO,CA4BjC;AAID,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,OAAO,GAAE,OAAO,CAAC,MAAM,CAAM,GAAG,MAAM,CAElE;AAED,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,YAAY,GAAG,SAAS,CAAC;CAC1D;AAED,sCAAsC;AACtC,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAYxE;AAED,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAgB1E;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAY3D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,UAAU,CAAC,EAAE,UAAU,GAAG,OAAO,CAMzF;AAID,sFAAsF;AACtF,wBAAsB,aAAa,CAAC,CAAC,SAAS,OAAO,EACpD,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,EACrB,GAAG,EAAE,CAAC,GACJ,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,YAAY,EAAE,OAAO,EAAE,CAAC"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @yaebal/test — testing utilities for yaebal bots.
|
|
3
|
+
*
|
|
4
|
+
* every plugin test used to hand-build fake updates and a mock api. this package
|
|
5
|
+
* extracts that boilerplate: {@link mockApi} records every call, the `*Update`
|
|
6
|
+
* factories produce real {@link Update} shapes, and {@link createContext} wraps
|
|
7
|
+
* one in a core {@link Context}.
|
|
8
|
+
*/
|
|
9
|
+
import { Context, } from "@yaebal/core";
|
|
10
|
+
/** default results for known methods; everything else resolves to `{}`. */
|
|
11
|
+
function defaultResult(method) {
|
|
12
|
+
if (method.startsWith("send") || method === "copyMessage" || method === "forwardMessage") {
|
|
13
|
+
return { message_id: 1 };
|
|
14
|
+
}
|
|
15
|
+
if (method === "answerCallbackQuery")
|
|
16
|
+
return true;
|
|
17
|
+
if (method === "getMe")
|
|
18
|
+
return { id: 1, is_bot: true, first_name: "bot", username: "bot" };
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* a fake {@link Api} whose every method records `{ method, params }` into `calls`
|
|
23
|
+
* and resolves to a sensible default (`{ message_id: 1 }` for `send*`, `true` for
|
|
24
|
+
* `answerCallbackQuery`, `{}` otherwise). hook registrars (`before`/`after`/
|
|
25
|
+
* `onError`) are no-ops that return the api for chaining.
|
|
26
|
+
*/
|
|
27
|
+
export function mockApi() {
|
|
28
|
+
const calls = [];
|
|
29
|
+
const record = (method, params) => {
|
|
30
|
+
calls.push({ method, params });
|
|
31
|
+
return Promise.resolve(defaultResult(method));
|
|
32
|
+
};
|
|
33
|
+
const registrar = {
|
|
34
|
+
call: (method, params) => record(method, params),
|
|
35
|
+
fileUrl: (filePath) => `https://example.invalid/file/${filePath}`,
|
|
36
|
+
before: () => api,
|
|
37
|
+
after: () => api,
|
|
38
|
+
onError: () => api,
|
|
39
|
+
};
|
|
40
|
+
const api = new Proxy(registrar, {
|
|
41
|
+
get(obj, prop) {
|
|
42
|
+
if (prop in obj)
|
|
43
|
+
return obj[prop];
|
|
44
|
+
const method = (params) => record(prop, params);
|
|
45
|
+
obj[prop] = method;
|
|
46
|
+
return method;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return { api, calls };
|
|
50
|
+
}
|
|
51
|
+
let updateIdCounter = 0;
|
|
52
|
+
/** build an {@link Update} from a partial, filling in a fresh `update_id`. */
|
|
53
|
+
export function createUpdate(partial = {}) {
|
|
54
|
+
return { update_id: ++updateIdCounter, ...partial };
|
|
55
|
+
}
|
|
56
|
+
/** build a message {@link Update}. */
|
|
57
|
+
export function messageUpdate(options = {}) {
|
|
58
|
+
const { text = "", chatId = 1, fromId = chatId, chatType = "private" } = options;
|
|
59
|
+
return createUpdate({
|
|
60
|
+
message: {
|
|
61
|
+
message_id: 1,
|
|
62
|
+
date: 0,
|
|
63
|
+
chat: { id: chatId, type: chatType },
|
|
64
|
+
from: { id: fromId, is_bot: false, first_name: "u" },
|
|
65
|
+
text,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** build a callback_query {@link Update}. */
|
|
70
|
+
export function callbackUpdate(options = {}) {
|
|
71
|
+
const { data = "", chatId = 1, fromId = chatId } = options;
|
|
72
|
+
return createUpdate({
|
|
73
|
+
callback_query: {
|
|
74
|
+
id: "1",
|
|
75
|
+
chat_instance: "0",
|
|
76
|
+
from: { id: fromId, is_bot: false, first_name: "u" },
|
|
77
|
+
message: {
|
|
78
|
+
message_id: 1,
|
|
79
|
+
date: 0,
|
|
80
|
+
chat: { id: chatId, type: "private" },
|
|
81
|
+
},
|
|
82
|
+
data,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** infer which payload key an update carries; defaults to `"message"`. */
|
|
87
|
+
export function detectUpdateType(update) {
|
|
88
|
+
if (update.message)
|
|
89
|
+
return "message";
|
|
90
|
+
if (update.edited_message)
|
|
91
|
+
return "edited_message";
|
|
92
|
+
if (update.channel_post)
|
|
93
|
+
return "channel_post";
|
|
94
|
+
if (update.callback_query)
|
|
95
|
+
return "callback_query";
|
|
96
|
+
const bag = update;
|
|
97
|
+
for (const key of Object.keys(bag)) {
|
|
98
|
+
if (key !== "update_id" && bag[key] !== undefined)
|
|
99
|
+
return key;
|
|
100
|
+
}
|
|
101
|
+
return "message";
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* wrap an {@link Update} in a core {@link Context}. the api defaults to a fresh
|
|
105
|
+
* {@link mockApi}; pass `updateType` to override the auto-detected one.
|
|
106
|
+
*/
|
|
107
|
+
export function createContext(update, api, updateType) {
|
|
108
|
+
return new Context({
|
|
109
|
+
api: api ?? mockApi().api,
|
|
110
|
+
update,
|
|
111
|
+
updateType: updateType ?? detectUpdateType(update),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const noop = async () => { };
|
|
115
|
+
/** run a composer's middleware against a context. resolves when the chain settles. */
|
|
116
|
+
export async function runMiddleware(composer, ctx) {
|
|
117
|
+
await composer.toMiddleware()(ctx, noop);
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Composer, Context } from "@yaebal/core";
|
|
4
|
+
import { callbackUpdate, createContext, createUpdate, messageUpdate, mockApi, runMiddleware, } from "./index.js";
|
|
5
|
+
test("mockApi records calls and resolves sensible defaults", async () => {
|
|
6
|
+
const { api, calls } = mockApi();
|
|
7
|
+
const sent = await api.sendMessage({ chat_id: 1, text: "hi" });
|
|
8
|
+
assert.deepEqual(sent, { message_id: 1 });
|
|
9
|
+
const answered = await api.answerCallbackQuery({ callback_query_id: "1" });
|
|
10
|
+
assert.equal(answered, true);
|
|
11
|
+
const viaCall = await api.call("setMyCommands", { commands: [] });
|
|
12
|
+
assert.deepEqual(viaCall, {});
|
|
13
|
+
assert.deepEqual(calls, [
|
|
14
|
+
{ method: "sendMessage", params: { chat_id: 1, text: "hi" } },
|
|
15
|
+
{ method: "answerCallbackQuery", params: { callback_query_id: "1" } },
|
|
16
|
+
{ method: "setMyCommands", params: { commands: [] } },
|
|
17
|
+
]);
|
|
18
|
+
});
|
|
19
|
+
test("mockApi hook registrars are chainable no-ops", () => {
|
|
20
|
+
const { api } = mockApi();
|
|
21
|
+
assert.equal(api.before(() => undefined), api);
|
|
22
|
+
assert.equal(api.after((_m, r) => r), api);
|
|
23
|
+
});
|
|
24
|
+
test("messageUpdate produces a valid message update", () => {
|
|
25
|
+
const update = messageUpdate({ text: "hello", chatId: 42, chatType: "group" });
|
|
26
|
+
assert.ok(update.update_id > 0);
|
|
27
|
+
assert.equal(update.message?.text, "hello");
|
|
28
|
+
assert.equal(update.message?.chat.id, 42);
|
|
29
|
+
assert.equal(update.message?.chat.type, "group");
|
|
30
|
+
assert.equal(update.message?.from?.id, 42);
|
|
31
|
+
});
|
|
32
|
+
test("callbackUpdate produces a valid callback_query update", () => {
|
|
33
|
+
const update = callbackUpdate({ data: "click", chatId: 7, fromId: 9 });
|
|
34
|
+
assert.equal(update.callback_query?.data, "click");
|
|
35
|
+
assert.equal(update.callback_query?.from.id, 9);
|
|
36
|
+
assert.equal(update.callback_query?.message?.chat.id, 7);
|
|
37
|
+
});
|
|
38
|
+
test("createUpdate fills a fresh update_id", () => {
|
|
39
|
+
const a = createUpdate();
|
|
40
|
+
const b = createUpdate();
|
|
41
|
+
assert.notEqual(a.update_id, b.update_id);
|
|
42
|
+
});
|
|
43
|
+
test("createContext yields a Context whose getters work", () => {
|
|
44
|
+
const ctx = createContext(messageUpdate({ text: "yo", chatId: 5 }));
|
|
45
|
+
assert.ok(ctx instanceof Context);
|
|
46
|
+
assert.equal(ctx.updateType, "message");
|
|
47
|
+
assert.equal(ctx.text, "yo");
|
|
48
|
+
assert.equal(ctx.chat?.id, 5);
|
|
49
|
+
});
|
|
50
|
+
test("createContext detects callback_query updates", () => {
|
|
51
|
+
const ctx = createContext(callbackUpdate({ data: "x" }));
|
|
52
|
+
assert.equal(ctx.updateType, "callback_query");
|
|
53
|
+
assert.equal(ctx.callbackQuery?.data, "x");
|
|
54
|
+
});
|
|
55
|
+
test("a handler calling ctx.reply records a sendMessage call", async () => {
|
|
56
|
+
const { api, calls } = mockApi();
|
|
57
|
+
const composer = new Composer().on("message:text", (ctx) => ctx.reply("pong"));
|
|
58
|
+
await runMiddleware(composer, createContext(messageUpdate({ text: "ping", chatId: 3 }), api));
|
|
59
|
+
assert.equal(calls.length, 1);
|
|
60
|
+
const call = calls[0];
|
|
61
|
+
assert.equal(call?.method, "sendMessage");
|
|
62
|
+
assert.equal(call?.params?.chat_id, 3);
|
|
63
|
+
assert.equal(call?.params?.text, "pong");
|
|
64
|
+
assert.deepEqual(call?.params?.reply_parameters, { message_id: 1 });
|
|
65
|
+
});
|
|
66
|
+
//# sourceMappingURL=index.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yaebal/test",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "yaebal test — testing utilities for yaebal bots: mock api, update factories, and context builders.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"types": "./lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"import": "./lib/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@yaebal/core": "0.0.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "latest"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"telegram",
|
|
29
|
+
"telegram-bot",
|
|
30
|
+
"yaebal",
|
|
31
|
+
"test",
|
|
32
|
+
"testing",
|
|
33
|
+
"mock"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/neverlane/yaebal",
|
|
39
|
+
"directory": "packages/test"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc -p tsconfig.json",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
|
+
"test": "node --test lib"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Composer, Context } from "@yaebal/core";
|
|
4
|
+
import {
|
|
5
|
+
callbackUpdate,
|
|
6
|
+
createContext,
|
|
7
|
+
createUpdate,
|
|
8
|
+
messageUpdate,
|
|
9
|
+
mockApi,
|
|
10
|
+
runMiddleware,
|
|
11
|
+
} from "./index.js";
|
|
12
|
+
|
|
13
|
+
test("mockApi records calls and resolves sensible defaults", async () => {
|
|
14
|
+
const { api, calls } = mockApi();
|
|
15
|
+
|
|
16
|
+
const sent = await api.sendMessage({ chat_id: 1, text: "hi" });
|
|
17
|
+
assert.deepEqual(sent, { message_id: 1 });
|
|
18
|
+
|
|
19
|
+
const answered = await api.answerCallbackQuery({ callback_query_id: "1" });
|
|
20
|
+
assert.equal(answered, true);
|
|
21
|
+
|
|
22
|
+
const viaCall = await api.call("setMyCommands", { commands: [] });
|
|
23
|
+
assert.deepEqual(viaCall, {});
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(calls, [
|
|
26
|
+
{ method: "sendMessage", params: { chat_id: 1, text: "hi" } },
|
|
27
|
+
{ method: "answerCallbackQuery", params: { callback_query_id: "1" } },
|
|
28
|
+
{ method: "setMyCommands", params: { commands: [] } },
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("mockApi hook registrars are chainable no-ops", () => {
|
|
33
|
+
const { api } = mockApi();
|
|
34
|
+
assert.equal(
|
|
35
|
+
api.before(() => undefined),
|
|
36
|
+
api,
|
|
37
|
+
);
|
|
38
|
+
assert.equal(
|
|
39
|
+
api.after((_m, r) => r),
|
|
40
|
+
api,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("messageUpdate produces a valid message update", () => {
|
|
45
|
+
const update = messageUpdate({ text: "hello", chatId: 42, chatType: "group" });
|
|
46
|
+
|
|
47
|
+
assert.ok(update.update_id > 0);
|
|
48
|
+
|
|
49
|
+
assert.equal(update.message?.text, "hello");
|
|
50
|
+
assert.equal(update.message?.chat.id, 42);
|
|
51
|
+
assert.equal(update.message?.chat.type, "group");
|
|
52
|
+
assert.equal(update.message?.from?.id, 42);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("callbackUpdate produces a valid callback_query update", () => {
|
|
56
|
+
const update = callbackUpdate({ data: "click", chatId: 7, fromId: 9 });
|
|
57
|
+
|
|
58
|
+
assert.equal(update.callback_query?.data, "click");
|
|
59
|
+
assert.equal(update.callback_query?.from.id, 9);
|
|
60
|
+
assert.equal(update.callback_query?.message?.chat.id, 7);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("createUpdate fills a fresh update_id", () => {
|
|
64
|
+
const a = createUpdate();
|
|
65
|
+
const b = createUpdate();
|
|
66
|
+
|
|
67
|
+
assert.notEqual(a.update_id, b.update_id);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("createContext yields a Context whose getters work", () => {
|
|
71
|
+
const ctx = createContext(messageUpdate({ text: "yo", chatId: 5 }));
|
|
72
|
+
|
|
73
|
+
assert.ok(ctx instanceof Context);
|
|
74
|
+
|
|
75
|
+
assert.equal(ctx.updateType, "message");
|
|
76
|
+
assert.equal(ctx.text, "yo");
|
|
77
|
+
assert.equal(ctx.chat?.id, 5);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("createContext detects callback_query updates", () => {
|
|
81
|
+
const ctx = createContext(callbackUpdate({ data: "x" }));
|
|
82
|
+
|
|
83
|
+
assert.equal(ctx.updateType, "callback_query");
|
|
84
|
+
assert.equal(ctx.callbackQuery?.data, "x");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a handler calling ctx.reply records a sendMessage call", async () => {
|
|
88
|
+
const { api, calls } = mockApi();
|
|
89
|
+
|
|
90
|
+
const composer = new Composer().on("message:text", (ctx) => ctx.reply("pong"));
|
|
91
|
+
await runMiddleware(composer, createContext(messageUpdate({ text: "ping", chatId: 3 }), api));
|
|
92
|
+
|
|
93
|
+
assert.equal(calls.length, 1);
|
|
94
|
+
|
|
95
|
+
const call = calls[0];
|
|
96
|
+
assert.equal(call?.method, "sendMessage");
|
|
97
|
+
assert.equal(call?.params?.chat_id, 3);
|
|
98
|
+
assert.equal(call?.params?.text, "pong");
|
|
99
|
+
|
|
100
|
+
assert.deepEqual(call?.params?.reply_parameters, { message_id: 1 });
|
|
101
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @yaebal/test — testing utilities for yaebal bots.
|
|
3
|
+
*
|
|
4
|
+
* every plugin test used to hand-build fake updates and a mock api. this package
|
|
5
|
+
* extracts that boilerplate: {@link mockApi} records every call, the `*Update`
|
|
6
|
+
* factories produce real {@link Update} shapes, and {@link createContext} wraps
|
|
7
|
+
* one in a core {@link Context}.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
type Api,
|
|
12
|
+
type Composer,
|
|
13
|
+
Context,
|
|
14
|
+
type Message,
|
|
15
|
+
type NextFn,
|
|
16
|
+
type Update,
|
|
17
|
+
type UpdateName,
|
|
18
|
+
} from "@yaebal/core";
|
|
19
|
+
|
|
20
|
+
/** a single recorded api call: the method name and the params it was given. */
|
|
21
|
+
export interface RecordedCall {
|
|
22
|
+
method: string;
|
|
23
|
+
params: Record<string, unknown> | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** result of {@link mockApi}: the fake `api` plus the array it records into. */
|
|
27
|
+
export interface MockApi {
|
|
28
|
+
api: Api;
|
|
29
|
+
calls: RecordedCall[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** default results for known methods; everything else resolves to `{}`. */
|
|
33
|
+
function defaultResult(method: string): unknown {
|
|
34
|
+
if (method.startsWith("send") || method === "copyMessage" || method === "forwardMessage") {
|
|
35
|
+
return { message_id: 1 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (method === "answerCallbackQuery") return true;
|
|
39
|
+
if (method === "getMe") return { id: 1, is_bot: true, first_name: "bot", username: "bot" };
|
|
40
|
+
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* a fake {@link Api} whose every method records `{ method, params }` into `calls`
|
|
46
|
+
* and resolves to a sensible default (`{ message_id: 1 }` for `send*`, `true` for
|
|
47
|
+
* `answerCallbackQuery`, `{}` otherwise). hook registrars (`before`/`after`/
|
|
48
|
+
* `onError`) are no-ops that return the api for chaining.
|
|
49
|
+
*/
|
|
50
|
+
export function mockApi(): MockApi {
|
|
51
|
+
const calls: RecordedCall[] = [];
|
|
52
|
+
|
|
53
|
+
const record = (method: string, params?: Record<string, unknown>): Promise<never> => {
|
|
54
|
+
calls.push({ method, params });
|
|
55
|
+
return Promise.resolve(defaultResult(method) as never);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const registrar: Record<string, unknown> = {
|
|
59
|
+
call: (method: string, params?: Record<string, unknown>) => record(method, params),
|
|
60
|
+
fileUrl: (filePath: string) => `https://example.invalid/file/${filePath}`,
|
|
61
|
+
before: () => api,
|
|
62
|
+
after: () => api,
|
|
63
|
+
onError: () => api,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const api = new Proxy(registrar, {
|
|
67
|
+
get(obj, prop: string) {
|
|
68
|
+
if (prop in obj) return obj[prop];
|
|
69
|
+
|
|
70
|
+
const method = (params?: Record<string, unknown>) => record(prop, params);
|
|
71
|
+
obj[prop] = method;
|
|
72
|
+
|
|
73
|
+
return method;
|
|
74
|
+
},
|
|
75
|
+
}) as unknown as Api;
|
|
76
|
+
|
|
77
|
+
return { api, calls };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let updateIdCounter = 0;
|
|
81
|
+
|
|
82
|
+
/** build an {@link Update} from a partial, filling in a fresh `update_id`. */
|
|
83
|
+
export function createUpdate(partial: Partial<Update> = {}): Update {
|
|
84
|
+
return { update_id: ++updateIdCounter, ...partial };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** options for {@link messageUpdate}. */
|
|
88
|
+
export interface MessageUpdateOptions {
|
|
89
|
+
text?: string;
|
|
90
|
+
chatId?: number;
|
|
91
|
+
fromId?: number;
|
|
92
|
+
chatType?: "private" | "group" | "supergroup" | "channel";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** build a message {@link Update}. */
|
|
96
|
+
export function messageUpdate(options: MessageUpdateOptions = {}): Update {
|
|
97
|
+
const { text = "", chatId = 1, fromId = chatId, chatType = "private" } = options;
|
|
98
|
+
|
|
99
|
+
return createUpdate({
|
|
100
|
+
message: {
|
|
101
|
+
message_id: 1,
|
|
102
|
+
date: 0,
|
|
103
|
+
chat: { id: chatId, type: chatType },
|
|
104
|
+
from: { id: fromId, is_bot: false, first_name: "u" },
|
|
105
|
+
text,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** options for {@link callbackUpdate}. */
|
|
111
|
+
export interface CallbackUpdateOptions {
|
|
112
|
+
data?: string;
|
|
113
|
+
chatId?: number;
|
|
114
|
+
fromId?: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** build a callback_query {@link Update}. */
|
|
118
|
+
export function callbackUpdate(options: CallbackUpdateOptions = {}): Update {
|
|
119
|
+
const { data = "", chatId = 1, fromId = chatId } = options;
|
|
120
|
+
|
|
121
|
+
return createUpdate({
|
|
122
|
+
callback_query: {
|
|
123
|
+
id: "1",
|
|
124
|
+
chat_instance: "0",
|
|
125
|
+
from: { id: fromId, is_bot: false, first_name: "u" },
|
|
126
|
+
message: {
|
|
127
|
+
message_id: 1,
|
|
128
|
+
date: 0,
|
|
129
|
+
chat: { id: chatId, type: "private" },
|
|
130
|
+
},
|
|
131
|
+
data,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** infer which payload key an update carries; defaults to `"message"`. */
|
|
137
|
+
export function detectUpdateType(update: Update): UpdateName {
|
|
138
|
+
if (update.message) return "message";
|
|
139
|
+
if (update.edited_message) return "edited_message";
|
|
140
|
+
if (update.channel_post) return "channel_post";
|
|
141
|
+
if (update.callback_query) return "callback_query";
|
|
142
|
+
|
|
143
|
+
const bag = update as unknown as Record<string, unknown>;
|
|
144
|
+
for (const key of Object.keys(bag)) {
|
|
145
|
+
if (key !== "update_id" && bag[key] !== undefined) return key as UpdateName;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return "message";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* wrap an {@link Update} in a core {@link Context}. the api defaults to a fresh
|
|
153
|
+
* {@link mockApi}; pass `updateType` to override the auto-detected one.
|
|
154
|
+
*/
|
|
155
|
+
export function createContext(update: Update, api?: Api, updateType?: UpdateName): Context {
|
|
156
|
+
return new Context({
|
|
157
|
+
api: api ?? mockApi().api,
|
|
158
|
+
update,
|
|
159
|
+
updateType: updateType ?? detectUpdateType(update),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const noop: NextFn = async () => {};
|
|
164
|
+
|
|
165
|
+
/** run a composer's middleware against a context. resolves when the chain settles. */
|
|
166
|
+
export async function runMiddleware<C extends Context>(
|
|
167
|
+
composer: Composer<C>,
|
|
168
|
+
ctx: C,
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
await composer.toMiddleware()(ctx, noop);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export type { Message };
|