feat: enable Telegram features on binding
This commit is contained in:
@@ -80,7 +80,7 @@ http://服务器地址:8180
|
|||||||
|
|
||||||
1. 在 Telegram 中联系 [@BotFather](https://t.me/BotFather),使用 `/newbot` 创建 Bot,并取得 Bot Token。
|
1. 在 Telegram 中联系 [@BotFather](https://t.me/BotFather),使用 `/newbot` 创建 Bot,并取得 Bot Token。
|
||||||
2. 打开“设置中心” → “Telegram”。
|
2. 打开“设置中心” → “Telegram”。
|
||||||
3. 填写 Bot Token,按需勾选“启用提醒”和“启用查询菜单”。
|
3. 填写 Bot Token。
|
||||||
4. 点击“验证并保存”。
|
4. 点击“验证并保存”。
|
||||||
5. 点击“生成绑定码”。
|
5. 点击“生成绑定码”。
|
||||||
6. 在 Telegram 中打开刚创建的 Bot,发送页面显示的命令,例如:
|
6. 在 Telegram 中打开刚创建的 Bot,发送页面显示的命令,例如:
|
||||||
@@ -91,7 +91,7 @@ http://服务器地址:8180
|
|||||||
|
|
||||||
7. 收到“绑定成功”后,返回页面点击“发送测试”。
|
7. 收到“绑定成功”后,返回页面点击“发送测试”。
|
||||||
|
|
||||||
绑定码十分钟内有效,一个实例只绑定一个 Telegram 会话。“启用提醒”只控制自动额度提醒,关闭后仍可发送测试消息;关闭查询菜单后 Bot 不再处理查询命令,并移除 Telegram 客户端键盘。即使关闭查询菜单,仍可完成绑定并接收已启用的提醒;开启菜单后可使用以下按钮或命令。`/account` 会向已绑定会话显示账号的完整邮箱,因此只应绑定受信任的私有会话。
|
绑定码十分钟内有效,一个实例只绑定一个 Telegram 会话。绑定成功后会自动启用额度提醒和查询菜单,可使用以下按钮或命令。`/account` 会向已绑定会话显示账号的完整邮箱,因此只应绑定受信任的私有会话。
|
||||||
|
|
||||||
- 当前用量(包含所有已添加连接)
|
- 当前用量(包含所有已添加连接)
|
||||||
- 重置时间 / `/reset`
|
- 重置时间 / `/reset`
|
||||||
@@ -101,7 +101,7 @@ http://服务器地址:8180
|
|||||||
|
|
||||||
服务器必须能够访问 `https://api.telegram.org`。
|
服务器必须能够访问 `https://api.telegram.org`。
|
||||||
|
|
||||||
点击“解除绑定”会删除当前 Bot Token、Chat ID、开关和未使用的绑定码。该操作不可恢复,之后需要重新填写 Token 并绑定。
|
点击“解除绑定”会删除当前 Bot Token、Chat ID 和未使用的绑定码。该操作不可恢复,之后需要重新填写 Token 并绑定。
|
||||||
|
|
||||||
## 配置 SMTP 邮件
|
## 配置 SMTP 邮件
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -88,11 +88,11 @@
|
|||||||
|
|
||||||
### Telegram
|
### Telegram
|
||||||
|
|
||||||
- `GET /api/v1/settings/telegram` 返回 `{chatId,enabled,menuEnabled,configured,botName?}`,不返回 Token 明文。
|
- `GET /api/v1/settings/telegram` 返回 `{chatId,enabled,menuEnabled,configured,botName?}`,不返回 Token 明文。`enabled` 和 `menuEnabled` 是兼容字段:`chatId != 0` 时两者始终为 `true`,未绑定时为 `false`。
|
||||||
- `PUT`(以及兼容保留的其他非 `GET`、非 `DELETE` 方法)接受 `{token,chatId,enabled,menuEnabled}`,Token 留空时保留旧值,保存前调用 Bot API `getMe` 验证。失败返回 400 或 502。菜单状态变化时响应可能包含 `warning`,表示设置已保存但 Telegram 客户端键盘同步失败。
|
- `PUT`(以及兼容保留的其他非 `GET`、非 `DELETE` 方法)接受 `{token,chatId,enabled,menuEnabled}`,Token 留空时保留旧值,保存前调用 Bot API `getMe` 验证。失败返回 400 或 502。兼容字段 `enabled` 和 `menuEnabled` 的请求值不能关闭已绑定 Bot 的提醒或查询菜单。
|
||||||
- `POST /api/v1/settings/telegram/bind` 返回六位 `{code}`,绑定码有效十分钟。
|
- `POST /api/v1/settings/telegram/bind` 返回六位 `{code}`,绑定码有效十分钟。
|
||||||
- `POST /api/v1/settings/telegram/test` 向已绑定会话发送测试消息;未绑定或发送失败返回 502。
|
- `POST /api/v1/settings/telegram/test` 向已绑定会话发送测试消息;未绑定或发送失败返回 502。
|
||||||
- `DELETE /api/v1/settings/telegram` 原子删除 Bot Token、Chat ID、Bot 信息、开关和绑定码,并重置 update offset;成功返回 `{ok:true,warning?}`。删除后移除 Telegram 客户端键盘失败只产生 `warning`,不恢复已删除的秘密。
|
- `DELETE /api/v1/settings/telegram` 原子删除 Bot Token、Chat ID、Bot 信息、兼容开关值和绑定码,并重置 update offset;成功返回 `{ok:true,warning?}`。删除后移除 Telegram 客户端键盘失败只产生 `warning`,不恢复已删除的秘密。
|
||||||
|
|
||||||
## 6. 维护接口
|
## 6. 维护接口
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ func (a *App) telegramSettings() TelegramSettings {
|
|||||||
t.Configured = token != ""
|
t.Configured = token != ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.Enabled = t.ChatID != 0
|
||||||
|
t.MenuEnabled = t.ChatID != 0
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
func (a *App) telegramSecret() (TelegramSettings, error) {
|
func (a *App) telegramSecret() (TelegramSettings, error) {
|
||||||
@@ -204,6 +206,8 @@ func (a *App) telegramAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
jsonOut(w, 400, map[string]string{"error": "Bot Token 必填"})
|
jsonOut(w, 400, map[string]string{"error": "Bot Token 必填"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
in.Enabled = in.ChatID != 0
|
||||||
|
in.MenuEnabled = in.ChatID != 0
|
||||||
var me struct {
|
var me struct {
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
Result struct {
|
Result struct {
|
||||||
@@ -309,7 +313,33 @@ func tgSend(t TelegramSettings, text string) error {
|
|||||||
}
|
}
|
||||||
return tgCall(t.Token, "sendMessage", params, &out)
|
return tgCall(t.Token, "sendMessage", params, &out)
|
||||||
}
|
}
|
||||||
|
func (a *App) syncLegacyTelegramMenu() {
|
||||||
|
a.telegramMu.Lock()
|
||||||
|
defer a.telegramMu.Unlock()
|
||||||
|
var t TelegramSettings
|
||||||
|
if !a.store.GetJSON("telegram", &t) || t.ChatID == 0 || t.MenuEnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
enc, ok := a.store.Get("telegram_token")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
token, err := a.vault.Decrypt(enc)
|
||||||
|
if err != nil || token == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Token = token
|
||||||
|
t.Configured = true
|
||||||
|
t.Enabled = true
|
||||||
|
t.MenuEnabled = true
|
||||||
|
if tgSend(t, "✅ <b>Codex 查询菜单已自动启用</b>\n\n现在可以使用菜单查询额度信息。") != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Token = ""
|
||||||
|
_ = a.store.SetJSON("telegram", t)
|
||||||
|
}
|
||||||
func (a *App) telegramLoop() {
|
func (a *App) telegramLoop() {
|
||||||
|
a.syncLegacyTelegramMenu()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-a.ctx.Done():
|
case <-a.ctx.Done():
|
||||||
@@ -367,16 +397,14 @@ func (a *App) handleTG(t TelegramSettings, chat int64, text string) {
|
|||||||
}
|
}
|
||||||
if a.store.GetJSON("telegram_bind", &b) && b.Expires > time.Now().Unix() && strings.TrimSpace(strings.TrimPrefix(text, "/bind ")) == b.Code {
|
if a.store.GetJSON("telegram_bind", &b) && b.Expires > time.Now().Unix() && strings.TrimSpace(strings.TrimPrefix(text, "/bind ")) == b.Code {
|
||||||
t.ChatID = chat
|
t.ChatID = chat
|
||||||
|
t.Enabled = true
|
||||||
|
t.MenuEnabled = true
|
||||||
safe := t
|
safe := t
|
||||||
safe.Token = ""
|
safe.Token = ""
|
||||||
_ = a.store.SetJSON("telegram", safe)
|
_ = a.store.SetJSON("telegram", safe)
|
||||||
_ = a.store.Set("telegram_bind", "{}")
|
_ = a.store.Set("telegram_bind", "{}")
|
||||||
t.ChatID = chat
|
t.ChatID = chat
|
||||||
message := "✅ <b>绑定成功</b>\n\nCodex 额度提醒将发送到此会话。"
|
_ = tgSend(t, "✅ <b>绑定成功</b>\n\n额度提醒和 Codex 额度查询菜单已启用。")
|
||||||
if t.MenuEnabled {
|
|
||||||
message = "✅ <b>绑定成功</b>\n\n现在可以使用菜单查询 Codex 额度信息。"
|
|
||||||
}
|
|
||||||
_ = tgSend(t, message)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ func TestTelegramDeleteWaitsForInFlightSaveAndRemainsFinal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDisabledTelegramSkipsAutomaticReminderButAllowsManualTest(t *testing.T) {
|
func TestBoundTelegramIgnoresLegacyDisabledFlags(t *testing.T) {
|
||||||
a := newReminderTestApp(t)
|
a := newReminderTestApp(t)
|
||||||
enc, err := a.vault.Encrypt("secret-token")
|
enc, err := a.vault.Encrypt("secret-token")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -212,14 +212,144 @@ func TestDisabledTelegramSkipsAutomaticReminderButAllowsManualTest(t *testing.T)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
a.sendPendingReminders(now)
|
a.sendPendingReminders(now)
|
||||||
if calls != 0 {
|
if calls != 1 {
|
||||||
t.Fatalf("automatic Telegram calls = %d; want 0", calls)
|
t.Fatalf("automatic Telegram calls = %d; want 1", calls)
|
||||||
}
|
}
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
a.telegramTest(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/settings/telegram/test", nil))
|
a.telegramTest(recorder, httptest.NewRequest(http.MethodPost, "/api/v1/settings/telegram/test", nil))
|
||||||
if recorder.Code != http.StatusOK || calls != 1 {
|
if recorder.Code != http.StatusOK || calls != 2 {
|
||||||
t.Fatalf("manual test status = %d calls = %d body = %s", recorder.Code, calls, recorder.Body.String())
|
t.Fatalf("manual test status = %d calls = %d body = %s", recorder.Code, calls, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
settings := a.telegramSettings()
|
||||||
|
if !settings.Enabled || !settings.MenuEnabled {
|
||||||
|
t.Fatalf("effective settings = %#v", settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLegacyBoundTelegramRestoresMenuOnce(t *testing.T) {
|
||||||
|
a := newReminderTestApp(t)
|
||||||
|
enc, err := a.vault.Encrypt("secret-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = a.store.Set("telegram_token", enc); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = a.store.SetJSON("telegram", TelegramSettings{ChatID: 123, Enabled: false, MenuEnabled: false, Configured: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
original := tgCall
|
||||||
|
t.Cleanup(func() { tgCall = original })
|
||||||
|
calls := 0
|
||||||
|
var sent map[string]any
|
||||||
|
tgCall = func(_ string, method string, params any, _ any) error {
|
||||||
|
if method != "sendMessage" {
|
||||||
|
t.Fatalf("method = %q", method)
|
||||||
|
}
|
||||||
|
calls++
|
||||||
|
body, marshalErr := json.Marshal(params)
|
||||||
|
if marshalErr != nil {
|
||||||
|
return marshalErr
|
||||||
|
}
|
||||||
|
return json.Unmarshal(body, &sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.syncLegacyTelegramMenu()
|
||||||
|
a.syncLegacyTelegramMenu()
|
||||||
|
if calls != 1 {
|
||||||
|
t.Fatalf("menu restore calls = %d; want 1", calls)
|
||||||
|
}
|
||||||
|
replyMarkup, ok := sent["reply_markup"].(map[string]any)
|
||||||
|
if !ok || replyMarkup["keyboard"] == nil {
|
||||||
|
t.Fatalf("send params = %#v", sent)
|
||||||
|
}
|
||||||
|
var stored TelegramSettings
|
||||||
|
if !a.store.GetJSON("telegram", &stored) || !stored.Enabled || !stored.MenuEnabled {
|
||||||
|
t.Fatalf("stored settings = %#v", stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTelegramSaveCannotDisableFeaturesForBoundChat(t *testing.T) {
|
||||||
|
a := newReminderTestApp(t)
|
||||||
|
original := tgCall
|
||||||
|
t.Cleanup(func() { tgCall = original })
|
||||||
|
tgCall = func(_ string, method string, _ any, out any) error {
|
||||||
|
if method == "sendMessage" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if method == "getMe" {
|
||||||
|
return json.Unmarshal([]byte(`{"ok":true,"result":{"first_name":"Test","username":"test_bot"}}`), out)
|
||||||
|
}
|
||||||
|
t.Fatalf("method = %q", method)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"token":"token","chatId":123,"enabled":false,"menuEnabled":false}`)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
a.telegramAPI(recorder, httptest.NewRequest(http.MethodPut, "/api/v1/settings/telegram", body))
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d body = %s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
var response TelegramSettings
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !response.Enabled || !response.MenuEnabled {
|
||||||
|
t.Fatalf("response = %#v", response)
|
||||||
|
}
|
||||||
|
var stored TelegramSettings
|
||||||
|
if !a.store.GetJSON("telegram", &stored) || !stored.Enabled || !stored.MenuEnabled {
|
||||||
|
t.Fatalf("stored settings = %#v", stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTelegramBindEnablesFeaturesAndSendsMenu(t *testing.T) {
|
||||||
|
a := newReminderTestApp(t)
|
||||||
|
enc, err := a.vault.Encrypt("secret-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = a.store.Set("telegram_token", enc); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = a.store.SetJSON("telegram", TelegramSettings{Configured: true}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = a.store.SetJSON("telegram_bind", map[string]any{"code": "123456", "expires": time.Now().Add(time.Minute).Unix()}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
original := tgCall
|
||||||
|
t.Cleanup(func() { tgCall = original })
|
||||||
|
var sent map[string]any
|
||||||
|
tgCall = func(_ string, method string, params any, _ any) error {
|
||||||
|
if method != "sendMessage" {
|
||||||
|
t.Fatalf("method = %q", method)
|
||||||
|
}
|
||||||
|
body, marshalErr := json.Marshal(params)
|
||||||
|
if marshalErr != nil {
|
||||||
|
return marshalErr
|
||||||
|
}
|
||||||
|
return json.Unmarshal(body, &sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.handleTG(a.telegramSecretForTest(t), 456, "/bind 123456")
|
||||||
|
settings := a.telegramSettings()
|
||||||
|
if settings.ChatID != 456 || !settings.Enabled || !settings.MenuEnabled {
|
||||||
|
t.Fatalf("settings = %#v", settings)
|
||||||
|
}
|
||||||
|
replyMarkup, ok := sent["reply_markup"].(map[string]any)
|
||||||
|
if !ok || replyMarkup["keyboard"] == nil {
|
||||||
|
t.Fatalf("send params = %#v", sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) telegramSecretForTest(t *testing.T) TelegramSettings {
|
||||||
|
t.Helper()
|
||||||
|
settings, err := a.telegramSecret()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
func reminderDashboard(fetchedAt int64, used float64, resetsAt int64) Dashboard {
|
func reminderDashboard(fetchedAt int64, used float64, resetsAt int64) Dashboard {
|
||||||
|
|||||||
@@ -27,6 +27,6 @@
|
|||||||
|
|
||||||
## Telegram 与 SMTP
|
## Telegram 与 SMTP
|
||||||
|
|
||||||
Telegram 保存加密 Token、Chat ID、启用开关和菜单开关。保存 Token 前调用 `getMe`;long polling timeout 为 25 秒,HTTP client timeout 为 35 秒。六位绑定码十分钟有效且一次成功后清除。只有绑定 Chat ID 且启用菜单时才处理查询命令;每条 update 在处理前重新核对当前 Token 和开关,关闭菜单会通过 `remove_keyboard` 清理客户端键盘。更换 Token 或删除配置会重置 update offset。解除绑定原子删除 Token、Chat ID、Bot 信息、开关和绑定码;随后清理 Telegram 键盘失败不会恢复本地秘密。
|
Telegram 保存加密 Token、Chat ID 和 Bot 信息。保存 Token 前调用 `getMe`;long polling timeout 为 25 秒,HTTP client timeout 为 35 秒。六位绑定码十分钟有效且一次成功后清除。存在绑定 Chat ID 时自动启用额度提醒和查询菜单;启动时如发现旧版本已绑定但关闭了菜单,会主动发送带键盘的启用通知,成功后写回新状态。每条 update 在处理前重新核对当前 Token 和 Chat ID。更换 Token 或删除配置会重置 update offset。解除绑定原子删除 Token、Chat ID、Bot 信息、兼容开关值和绑定码;随后清理 Telegram 键盘失败不会恢复本地秘密。
|
||||||
|
|
||||||
SMTP 支持 `starttls`、隐式 `tls` 和 `none`,TLS 最低 1.2;支持可选 PLAIN AUTH,发送 multipart text/html。TCP 连接和后续 SMTP/TLS 读写共享 35 秒 deadline。修改外部调用时必须保留这一超时边界、TLS server name、HTML 转义和不记录秘密的错误处理。
|
SMTP 支持 `starttls`、隐式 `tls` 和 `none`,TLS 最低 1.2;支持可选 PLAIN AUTH,发送 multipart text/html。TCP 连接和后续 SMTP/TLS 读写共享 35 秒 deadline。修改外部调用时必须保留这一超时边界、TLS server name、HTML 转义和不记录秘密的错误处理。
|
||||||
|
|||||||
+2
-23
@@ -1026,28 +1026,7 @@ function Telegram() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="checks">
|
<p className="hint">Bot 绑定后会自动启用额度提醒和查询菜单。</p>
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={v.enabled}
|
|
||||||
onChange={(e) => setV({ ...v, enabled: e.target.checked })}
|
|
||||||
/>
|
|
||||||
启用提醒
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={v.menuEnabled}
|
|
||||||
onChange={(e) => setV({ ...v, menuEnabled: e.target.checked })}
|
|
||||||
/>
|
|
||||||
启用查询菜单
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<p className="hint telegram-switch-hint">
|
|
||||||
“启用提醒”只控制自动额度提醒,关闭后仍可发送测试消息;“启用查询菜单”关闭后将停止查询并移除
|
|
||||||
Telegram 键盘。
|
|
||||||
</p>
|
|
||||||
<button>验证并保存</button>
|
<button>验证并保存</button>
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<button
|
<button
|
||||||
@@ -1085,7 +1064,7 @@ function Telegram() {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
"确定要解除 Telegram Bot 绑定吗?这会删除 Bot Token、Chat ID、开关和未使用的绑定码。",
|
"确定要解除 Telegram Bot 绑定吗?这会删除 Bot Token、Chat ID 和未使用的绑定码。",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -478,11 +478,6 @@ header p {
|
|||||||
.checks input {
|
.checks input {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
.telegram-switch-hint {
|
|
||||||
margin: -8px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ test("shows the GitHub link after login", async ({ page }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("saves disabled Telegram switches and removes the configuration", async ({
|
test("automatically enables Telegram features and removes the configuration", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
let savedBody: Record<string, unknown> | undefined;
|
let savedBody: Record<string, unknown> | undefined;
|
||||||
@@ -162,11 +162,14 @@ test("saves disabled Telegram switches and removes the configuration", async ({
|
|||||||
page.on("dialog", (dialog) => dialog.accept());
|
page.on("dialog", (dialog) => dialog.accept());
|
||||||
await page.goto("/settings");
|
await page.goto("/settings");
|
||||||
await page.getByRole("tab", { name: "Telegram" }).click();
|
await page.getByRole("tab", { name: "Telegram" }).click();
|
||||||
await page.getByLabel("启用提醒").uncheck();
|
await expect(page.getByLabel("启用提醒")).toHaveCount(0);
|
||||||
await page.getByLabel("启用查询菜单").uncheck();
|
await expect(page.getByLabel("启用查询菜单")).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.getByText("Bot 绑定后会自动启用额度提醒和查询菜单。"),
|
||||||
|
).toBeVisible();
|
||||||
await page.getByRole("button", { name: "验证并保存" }).click();
|
await page.getByRole("button", { name: "验证并保存" }).click();
|
||||||
await expect.poll(() => savedBody?.enabled).toBe(false);
|
await expect.poll(() => savedBody?.enabled).toBe(true);
|
||||||
expect(savedBody?.menuEnabled).toBe(false);
|
expect(savedBody?.menuEnabled).toBe(true);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "解除绑定" }).click();
|
await page.getByRole("button", { name: "解除绑定" }).click();
|
||||||
await expect.poll(() => deleted).toBe(true);
|
await expect.poll(() => deleted).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user