fix(settings): Telegram 检测/测试闭环与 8 项告警开关恢复
检测与测试前自动持久化 Token/Chat ID;getUpdates 清除 Webhook 并返回具体 API 错误;恢复 NOTIFY 分项开关与免密 reveal。
This commit is contained in:
@@ -376,3 +376,23 @@
|
|||||||
{"ts":"2026-06-07T19:34:14+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
{"ts":"2026-06-07T19:34:14+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
{"ts":"2026-06-07T19:34:14+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
{"ts":"2026-06-07T19:34:14+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
{"ts":"2026-06-07T19:34:14+08:00","date":"2026-06-07","gate":"summary","result":"PASS","detail":"7/7"}
|
{"ts":"2026-06-07T19:34:14+08:00","date":"2026-06-07","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"changelog","result":"PASS","detail":"2026-06-07-settings-telegram-fix.md 37lines"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"audit","result":"PASS","detail":"2026-06-07-settings-telegram-fix.md"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"test","result":"BLOCK","detail":"tests failed"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-07T19:44:37+08:00","date":"2026-06-07","gate":"summary","result":"BLOCK","detail":"6/7"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"changelog","result":"PASS","detail":"2026-06-07-settings-telegram-fix.md 37lines"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"audit","result":"PASS","detail":"2026-06-07-settings-telegram-fix.md"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"test","result":"PASS","detail":"all passed"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"lint","result":"PASS","detail":"0 violations"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"import","result":"PASS","detail":"ok"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"security","result":"PASS","detail":"0 HIGH"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"review","result":"PASS","detail":"audit covers changes"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"shell_eol","result":"PASS","detail":"all tracked sh LF"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"text_eol","result":"PASS","detail":"git index LF"}
|
||||||
|
{"ts":"2026-06-07T19:44:50+08:00","date":"2026-06-07","gate":"summary","result":"PASS","detail":"7/7"}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# 审计 — 设置页 Telegram 全流程修复
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
| 文件 | 变更 |
|
||||||
|
|------|------|
|
||||||
|
| `frontend/src/pages/SettingsPage.vue` | persist/save/test/detect、8 NOTIFY 开关、reveal 免密 |
|
||||||
|
| `frontend/src/types/api.ts` | `NOTIFY_TOGGLE_ITEMS` |
|
||||||
|
| `server/api/settings.py` | Webhook 清除、错误详情、reveal 免密 |
|
||||||
|
| `tests/test_settings_telegram.py` | 6 用例 |
|
||||||
|
| `deploy/gate_log.jsonl` | 门控记录(自动生成) |
|
||||||
|
|
||||||
|
## Step 3 规则扫描
|
||||||
|
|
||||||
|
| H | 规则 | 结论 |
|
||||||
|
|---|------|------|
|
||||||
|
| H1 | 测试/检测前持久化 Token+Chat ID | FIXED — `persistTelegramConfig` |
|
||||||
|
| H2 | getUpdates Webhook 冲突 | FIXED — `deleteWebhook` before poll |
|
||||||
|
| H3 | Telegram API 错误可见 | FIXED — `ok:false` / sendMessage description |
|
||||||
|
| H4 | 8 项 NOTIFY 开关 UI | FIXED — Vue v-switch + PUT |
|
||||||
|
| H5 | reveal 免密(用户确认) | FIXED — JWT + 审计保留 |
|
||||||
|
|
||||||
|
## Closure
|
||||||
|
|
||||||
|
| H | 判定 | 依据 |
|
||||||
|
|---|------|------|
|
||||||
|
| H1–H5 | FIXED | 代码 + pytest 6/6 |
|
||||||
|
|
||||||
|
## DoD
|
||||||
|
|
||||||
|
- [x] pytest `test_settings_telegram.py` 6 passed
|
||||||
|
- [x] changelog / audit
|
||||||
|
- [x] 无不可变 key 变更
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
`.venv/bin/pytest tests/test_settings_telegram.py -q`
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Changelog — 设置页 Telegram 全流程修复 + 告警开关恢复
|
||||||
|
|
||||||
|
**日期**:2026-06-07
|
||||||
|
|
||||||
|
## 摘要
|
||||||
|
|
||||||
|
修复设置页 Telegram 推送相关功能:检测/测试前自动持久化配置、独立保存按钮、getUpdates Webhook 冲突处理、测试 API 返回 Telegram 具体错误;恢复 Vue 迁移遗漏的 8 项告警通知开关;API Key / Bot Token 查看免二次密码(JWT + 审计仍保留)。
|
||||||
|
|
||||||
|
## 动机
|
||||||
|
|
||||||
|
用户反馈「检测 Chat ID / 发送测试 / 多项功能无法正常使用」:测试按钮不保存草稿配置、告警分项开关在 SPA 中缺失、getUpdates 在 Bot 曾设 Webhook 时恒为空、错误提示过于笼统。
|
||||||
|
|
||||||
|
## 涉及文件
|
||||||
|
|
||||||
|
- `frontend/src/pages/SettingsPage.vue` — `persistTelegramConfig`、保存按钮、8 项 NOTIFY 开关
|
||||||
|
- `frontend/src/types/api.ts` — `NOTIFY_TOGGLE_ITEMS`
|
||||||
|
- `server/api/settings.py` — Webhook 清除、Telegram 错误详情、reveal 免密
|
||||||
|
- `tests/test_settings_telegram.py` — 新增
|
||||||
|
- `docs/audit/2026-06-07-settings-telegram-fix.md`
|
||||||
|
|
||||||
|
## 迁移 / 重启
|
||||||
|
|
||||||
|
- 无 DB 迁移
|
||||||
|
- 需重新构建前端并重启 API(或全量部署)
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/pytest tests/test_settings_telegram.py -q
|
||||||
|
bash scripts/local_verify.sh
|
||||||
|
bash deploy/pre_deploy_check.sh
|
||||||
|
# 设置页:填 Token → 检测 Chat ID → 测试发送 → 切换告警开关
|
||||||
|
```
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
Layer-3 cron(`health_monitor.sh`)仍读 `NEXUS_TELEGRAM_*`(`.env.prod`),与 UI 保存的 MySQL 配置独立;应用内告警走 MySQL 热更新即可。
|
||||||
@@ -63,18 +63,42 @@
|
|||||||
class="mb-3"
|
class="mb-3"
|
||||||
:placeholder="telegramTokenSet ? '已配置(留空保持不变)' : '输入 Bot Token'"
|
:placeholder="telegramTokenSet ? '已配置(留空保持不变)' : '输入 Bot Token'"
|
||||||
:type="showToken ? 'text' : 'password'"
|
:type="showToken ? 'text' : 'password'"
|
||||||
:append-inner-icon="showToken ? 'mdi-eye' : 'mdi-eye-off'"
|
:append-inner-icon="showToken ? 'mdi-eye-off' : 'mdi-eye'"
|
||||||
@click:append-inner="revealTelegramToken"
|
@click:append-inner="toggleTelegramTokenVisibility"
|
||||||
/>
|
/>
|
||||||
<v-chip v-if="telegramTokenSet && !telegramTokenDraft" size="small" color="success" variant="tonal" class="mb-3">Token 已配置</v-chip>
|
<v-chip v-if="telegramTokenSet && !telegramTokenDraft" size="small" color="success" variant="tonal" class="mb-3">Token 已配置</v-chip>
|
||||||
<v-text-field v-model="settings.telegram_chat_id" label="Chat ID" variant="outlined" density="compact" class="mb-3" />
|
<v-text-field v-model="settings.telegram_chat_id" label="Chat ID" variant="outlined" density="compact" class="mb-3" />
|
||||||
<p class="text-caption text-medium-emphasis mb-3">
|
<p class="text-caption text-medium-emphasis mb-3">
|
||||||
在 Telegram 向 Bot 发送任意消息后,可点击下方按钮自动检测 Chat ID。
|
在 Telegram 向 Bot 发送任意消息后,可点击下方按钮自动检测 Chat ID。
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex flex-wrap ga-2">
|
<div class="d-flex flex-wrap ga-2 mb-4">
|
||||||
<v-btn size="small" variant="tonal" prepend-icon="mdi-radar" @click="detectTelegramChats" :loading="detectingTgChats">检测 Chat ID</v-btn>
|
<v-btn size="small" variant="tonal" prepend-icon="mdi-radar" @click="detectTelegramChats" :loading="detectingTgChats">检测 Chat ID</v-btn>
|
||||||
<v-btn size="small" variant="tonal" prepend-icon="mdi-message-text" @click="testTelegram" :loading="testingTg">发送测试消息</v-btn>
|
<v-btn size="small" variant="tonal" prepend-icon="mdi-message-text" @click="testTelegram" :loading="testingTg">发送测试消息</v-btn>
|
||||||
|
<v-btn size="small" variant="tonal" color="primary" prepend-icon="mdi-content-save" @click="saveTelegramConfig" :loading="savingTelegram">保存 Telegram 配置</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="my-3" />
|
||||||
|
<div class="text-subtitle-2 mb-1">告警通知项目</div>
|
||||||
|
<p class="text-caption text-medium-emphasis mb-3">
|
||||||
|
关闭后该类告警仍记录日志,但不发送 Telegram 消息。Bot Token 未配置时全部静默。
|
||||||
|
</p>
|
||||||
|
<template v-for="(groupItems, groupName) in notifyToggleGroups" :key="groupName">
|
||||||
|
<div class="text-caption text-medium-emphasis font-weight-medium mt-2 mb-1">{{ groupName }}</div>
|
||||||
|
<v-switch
|
||||||
|
v-for="item in groupItems"
|
||||||
|
:key="item.key"
|
||||||
|
:model-value="notifyToggles[item.key] ?? true"
|
||||||
|
:label="item.label"
|
||||||
|
:hint="item.desc"
|
||||||
|
persistent-hint
|
||||||
|
color="primary"
|
||||||
|
density="compact"
|
||||||
|
hide-details="auto"
|
||||||
|
class="mb-1"
|
||||||
|
:disabled="notifyToggleSaving === item.key"
|
||||||
|
@update:model-value="(v: boolean | null) => toggleNotify(item.key, Boolean(v))"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
@@ -154,7 +178,7 @@
|
|||||||
<v-card-text>
|
<v-card-text>
|
||||||
<div class="d-flex align-center ga-2">
|
<div class="d-flex align-center ga-2">
|
||||||
<v-text-field :model-value="showApiKey ? apiKeyValue : '••••••••'" label="API Key" variant="outlined" density="compact" readonly :type="showApiKey ? 'text' : 'password'" style="max-width: 320px" />
|
<v-text-field :model-value="showApiKey ? apiKeyValue : '••••••••'" label="API Key" variant="outlined" density="compact" readonly :type="showApiKey ? 'text' : 'password'" style="max-width: 320px" />
|
||||||
<v-btn variant="text" size="small" :icon="showApiKey ? 'mdi-eye-off' : 'mdi-eye'" @click="revealApiKey" />
|
<v-btn variant="text" size="small" :icon="showApiKey ? 'mdi-eye-off' : 'mdi-eye'" @click="revealApiKey" :loading="revealingApiKey" />
|
||||||
<v-btn variant="text" size="small" icon="mdi-content-copy" @click="copyApiKey" />
|
<v-btn variant="text" size="small" icon="mdi-content-copy" @click="copyApiKey" />
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -250,24 +274,6 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<!-- API Key Reveal Dialog -->
|
|
||||||
<v-dialog v-model="showRevealDialog" max-width="400">
|
|
||||||
<v-card border>
|
|
||||||
<v-card-title>{{ revealTarget === 'telegram' ? '查看 Bot Token' : '验证身份' }}</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<div class="text-body-2 text-medium-emphasis mb-3">
|
|
||||||
{{ revealTarget === 'telegram' ? '请输入当前密码以查看 Telegram Bot Token' : '请输入当前密码以查看 API Key' }}
|
|
||||||
</div>
|
|
||||||
<v-text-field v-model="revealPassword" label="当前密码" variant="outlined" density="compact" type="password" autofocus @keydown.enter="doRevealApiKey" :rules="[required('密码')]" />
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-spacer />
|
|
||||||
<v-btn variant="text" @click="showRevealDialog = false">取消</v-btn>
|
|
||||||
<v-btn color="primary" variant="flat" @click="doRevealSecret" :loading="revealingKey">确认</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<!-- Telegram Chat ID picker -->
|
<!-- Telegram Chat ID picker -->
|
||||||
<v-dialog v-model="tgChatDialog" max-width="480">
|
<v-dialog v-model="tgChatDialog" max-width="480">
|
||||||
<v-card border>
|
<v-card border>
|
||||||
@@ -327,6 +333,7 @@ import type {
|
|||||||
SubscriptionParseResponse,
|
SubscriptionParseResponse,
|
||||||
IpAllowlistSaveRequest,
|
IpAllowlistSaveRequest,
|
||||||
} from '@/types/api'
|
} from '@/types/api'
|
||||||
|
import { NOTIFY_TOGGLE_ITEMS } from '@/types/api'
|
||||||
|
|
||||||
const snackbar = useSnackbar()
|
const snackbar = useSnackbar()
|
||||||
|
|
||||||
@@ -348,6 +355,23 @@ const detectingTgChats = ref(false)
|
|||||||
const tgChatDialog = ref(false)
|
const tgChatDialog = ref(false)
|
||||||
const tgChats = ref<TelegramChatItem[]>([])
|
const tgChats = ref<TelegramChatItem[]>([])
|
||||||
const showToken = ref(false)
|
const showToken = ref(false)
|
||||||
|
const savingTelegram = ref(false)
|
||||||
|
const notifyToggles = ref<Record<string, boolean>>({})
|
||||||
|
const notifyToggleSaving = ref<string | null>(null)
|
||||||
|
|
||||||
|
const notifyToggleGroups = computed(() => {
|
||||||
|
const groups: Record<string, typeof NOTIFY_TOGGLE_ITEMS> = {}
|
||||||
|
for (const item of NOTIFY_TOGGLE_ITEMS) {
|
||||||
|
if (!groups[item.group]) groups[item.group] = []
|
||||||
|
groups[item.group].push(item)
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
})
|
||||||
|
|
||||||
|
function parseNotifyEnabled(val: unknown): boolean {
|
||||||
|
const s = String(val ?? 'true').trim().toLowerCase()
|
||||||
|
return !['false', '0', 'no', 'off'].includes(s)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Password ──
|
// ── Password ──
|
||||||
const pwForm = ref({ current: '', new_pw: '', confirm: '', totp_code: '' })
|
const pwForm = ref({ current: '', new_pw: '', confirm: '', totp_code: '' })
|
||||||
@@ -366,10 +390,8 @@ const disablingTotp = ref(false)
|
|||||||
// ── API Key ──
|
// ── API Key ──
|
||||||
const showApiKey = ref(false)
|
const showApiKey = ref(false)
|
||||||
const apiKeyValue = ref('')
|
const apiKeyValue = ref('')
|
||||||
const showRevealDialog = ref(false)
|
const revealingApiKey = ref(false)
|
||||||
const revealTarget = ref<'api_key' | 'telegram'>('api_key')
|
const revealingTelegramToken = ref(false)
|
||||||
const revealPassword = ref('')
|
|
||||||
const revealingKey = ref(false)
|
|
||||||
|
|
||||||
// ── IP Allowlist ──
|
// ── IP Allowlist ──
|
||||||
const ipAllowlistEnabled = ref(false)
|
const ipAllowlistEnabled = ref(false)
|
||||||
@@ -399,6 +421,10 @@ async function loadSettings() {
|
|||||||
telegramTokenDraft.value = ''
|
telegramTokenDraft.value = ''
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (key && NOTIFY_TOGGLE_ITEMS.some(n => n.key === key)) {
|
||||||
|
notifyToggles.value[key] = parseNotifyEnabled(item.value)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (key && knownKeys.includes(key)) {
|
if (key && knownKeys.includes(key)) {
|
||||||
const val = item.value
|
const val = item.value
|
||||||
// Type-cast numeric strings back to numbers for number inputs
|
// Type-cast numeric strings back to numbers for number inputs
|
||||||
@@ -435,32 +461,81 @@ async function loadAllowlist() {
|
|||||||
// ── Actions ──
|
// ── Actions ──
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
const tokenUpdate = telegramTokenDraft.value.trim()
|
|
||||||
try {
|
try {
|
||||||
const entries = Object.entries(settings.value).filter(([key]) =>
|
const entries = Object.entries(settings.value).filter(([key]) =>
|
||||||
['system_name', 'system_title', 'cpu_alert_threshold', 'mem_alert_threshold', 'disk_alert_threshold', 'db_pool_size', 'db_max_overflow', 'telegram_chat_id'].includes(key)
|
['system_name', 'system_title', 'cpu_alert_threshold', 'mem_alert_threshold', 'disk_alert_threshold', 'db_pool_size', 'db_max_overflow'].includes(key)
|
||||||
)
|
)
|
||||||
if (tokenUpdate) {
|
|
||||||
entries.push(['telegram_bot_token', tokenUpdate])
|
|
||||||
}
|
|
||||||
for (const [key, value] of entries) {
|
for (const [key, value] of entries) {
|
||||||
await http.put(`/settings/${key}`, { value })
|
await http.put(`/settings/${key}`, { value })
|
||||||
}
|
}
|
||||||
snackbar('设置已保存')
|
snackbar('设置已保存')
|
||||||
if (tokenUpdate) {
|
|
||||||
telegramTokenSet.value = true
|
|
||||||
telegramTokenDraft.value = ''
|
|
||||||
showToken.value = false
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const msg = e instanceof Error ? e.message : '保存失败'
|
const msg = e instanceof Error ? e.message : '保存失败'
|
||||||
snackbar(msg, 'error')
|
snackbar(msg, 'error')
|
||||||
} finally { saving.value = false }
|
} finally { saving.value = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function persistTelegramConfig(options?: { requireToken?: boolean; requireChatId?: boolean }): Promise<boolean> {
|
||||||
|
const tokenUpdate = telegramTokenDraft.value.trim()
|
||||||
|
const chatId = String(settings.value.telegram_chat_id || '').trim()
|
||||||
|
|
||||||
|
if (options?.requireToken && !tokenUpdate && !telegramTokenSet.value) {
|
||||||
|
snackbar('请先填写 Bot Token', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (options?.requireChatId && !chatId) {
|
||||||
|
snackbar('请先填写或检测 Chat ID', 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tokenUpdate) {
|
||||||
|
await http.put('/settings/telegram_bot_token', { value: tokenUpdate })
|
||||||
|
telegramTokenSet.value = true
|
||||||
|
telegramTokenDraft.value = ''
|
||||||
|
showToken.value = false
|
||||||
|
}
|
||||||
|
if (chatId) {
|
||||||
|
await http.put('/settings/telegram_chat_id', { value: chatId })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveTelegramConfig() {
|
||||||
|
savingTelegram.value = true
|
||||||
|
try {
|
||||||
|
const tokenUpdate = telegramTokenDraft.value.trim()
|
||||||
|
const chatId = String(settings.value.telegram_chat_id || '').trim()
|
||||||
|
if (!tokenUpdate && !chatId && !telegramTokenSet.value) {
|
||||||
|
snackbar('请先填写 Bot Token 或 Chat ID', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await persistTelegramConfig()
|
||||||
|
snackbar('Telegram 配置已保存')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
snackbar(e instanceof Error ? e.message : '保存失败', 'error')
|
||||||
|
} finally {
|
||||||
|
savingTelegram.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleNotify(key: string, enabled: boolean) {
|
||||||
|
const prev = notifyToggles.value[key]
|
||||||
|
notifyToggles.value[key] = enabled
|
||||||
|
notifyToggleSaving.value = key
|
||||||
|
try {
|
||||||
|
await http.put(`/settings/${key}`, { value: enabled ? 'true' : 'false' })
|
||||||
|
} catch (e: unknown) {
|
||||||
|
notifyToggles.value[key] = prev
|
||||||
|
snackbar(e instanceof Error ? e.message : '保存失败,已撤销', 'error')
|
||||||
|
} finally {
|
||||||
|
notifyToggleSaving.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function testTelegram() {
|
async function testTelegram() {
|
||||||
testingTg.value = true
|
testingTg.value = true
|
||||||
try {
|
try {
|
||||||
|
if (!await persistTelegramConfig({ requireToken: true, requireChatId: true })) return
|
||||||
await http.post('/settings/telegram/test')
|
await http.post('/settings/telegram/test')
|
||||||
snackbar('测试消息已发送')
|
snackbar('测试消息已发送')
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -471,16 +546,7 @@ async function testTelegram() {
|
|||||||
async function detectTelegramChats() {
|
async function detectTelegramChats() {
|
||||||
detectingTgChats.value = true
|
detectingTgChats.value = true
|
||||||
try {
|
try {
|
||||||
const tokenUpdate = telegramTokenDraft.value.trim()
|
if (!await persistTelegramConfig({ requireToken: true })) return
|
||||||
if (tokenUpdate) {
|
|
||||||
await http.put('/settings/telegram_bot_token', { value: tokenUpdate })
|
|
||||||
telegramTokenSet.value = true
|
|
||||||
telegramTokenDraft.value = ''
|
|
||||||
showToken.value = false
|
|
||||||
} else if (!telegramTokenSet.value) {
|
|
||||||
snackbar('请先填写 Bot Token', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const res = await http.get<TelegramChatsResponse>('/settings/telegram/chats')
|
const res = await http.get<TelegramChatsResponse>('/settings/telegram/chats')
|
||||||
tgChats.value = res.chats || []
|
tgChats.value = res.chats || []
|
||||||
if (!tgChats.value.length) {
|
if (!tgChats.value.length) {
|
||||||
@@ -586,61 +652,53 @@ async function revealApiKey() {
|
|||||||
showApiKey.value = false
|
showApiKey.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
revealTarget.value = 'api_key'
|
revealingApiKey.value = true
|
||||||
showRevealDialog.value = true
|
|
||||||
revealPassword.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function revealTelegramToken() {
|
|
||||||
if (showToken.value && telegramTokenDraft.value) {
|
|
||||||
showToken.value = false
|
|
||||||
telegramTokenDraft.value = ''
|
|
||||||
return
|
|
||||||
}
|
|
||||||
revealTarget.value = 'telegram'
|
|
||||||
showRevealDialog.value = true
|
|
||||||
revealPassword.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doRevealSecret() {
|
|
||||||
if (!revealPassword.value) {
|
|
||||||
snackbar('请输入密码', 'error')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
revealingKey.value = true
|
|
||||||
try {
|
try {
|
||||||
if (revealTarget.value === 'telegram') {
|
const res = await http.post<{ value: string }>('/settings/api-key/reveal', {})
|
||||||
const res = await http.post<{ value: string }>('/settings/telegram/reveal-token', {
|
apiKeyValue.value = res.value
|
||||||
current_password: revealPassword.value,
|
showApiKey.value = true
|
||||||
})
|
|
||||||
telegramTokenDraft.value = res.value
|
|
||||||
showToken.value = true
|
|
||||||
} else {
|
|
||||||
const res = await http.post<{ api_key: string }>('/settings/api-key/reveal', {
|
|
||||||
current_password: revealPassword.value,
|
|
||||||
})
|
|
||||||
apiKeyValue.value = res.api_key
|
|
||||||
showApiKey.value = true
|
|
||||||
}
|
|
||||||
showRevealDialog.value = false
|
|
||||||
revealPassword.value = ''
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
snackbar(e instanceof Error ? e.message : '验证失败', 'error')
|
snackbar(e instanceof Error ? e.message : '获取 API Key 失败', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
revealingKey.value = false
|
revealingApiKey.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doRevealApiKey() {
|
async function toggleTelegramTokenVisibility() {
|
||||||
revealTarget.value = 'api_key'
|
if (showToken.value) {
|
||||||
await doRevealSecret()
|
showToken.value = false
|
||||||
|
if (telegramTokenSet.value) {
|
||||||
|
telegramTokenDraft.value = ''
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (telegramTokenDraft.value.trim()) {
|
||||||
|
showToken.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!telegramTokenSet.value) {
|
||||||
|
showToken.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
revealingTelegramToken.value = true
|
||||||
|
try {
|
||||||
|
const res = await http.post<{ value: string }>('/settings/telegram/reveal-token', {})
|
||||||
|
telegramTokenDraft.value = res.value
|
||||||
|
showToken.value = true
|
||||||
|
} catch (e: unknown) {
|
||||||
|
snackbar(e instanceof Error ? e.message : '获取 Bot Token 失败', 'error')
|
||||||
|
} finally {
|
||||||
|
revealingTelegramToken.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyApiKey() {
|
function copyApiKey() {
|
||||||
if (apiKeyValue.value) {
|
if (!showApiKey.value || !apiKeyValue.value) {
|
||||||
navigator.clipboard.writeText(apiKeyValue.value)
|
snackbar('请先点击眼睛图标显示 API Key', 'warning')
|
||||||
snackbar('已复制到剪贴板')
|
return
|
||||||
}
|
}
|
||||||
|
navigator.clipboard.writeText(apiKeyValue.value)
|
||||||
|
snackbar('已复制到剪贴板')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleAllowlist() {
|
async function toggleAllowlist() {
|
||||||
|
|||||||
@@ -35,6 +35,25 @@ export interface TelegramChatsResponse {
|
|||||||
count: number
|
count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Telegram notification toggle (settings key → label) */
|
||||||
|
export interface NotifyToggleItem {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
desc: string
|
||||||
|
group: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NOTIFY_TOGGLE_ITEMS: NotifyToggleItem[] = [
|
||||||
|
{ key: 'notify_alert_cpu', label: 'CPU 超阈值告警', desc: 'CPU 使用率超过设定阈值时推送', group: '服务器资源' },
|
||||||
|
{ key: 'notify_alert_mem', label: '内存超阈值告警', desc: '内存使用率超过设定阈值时推送', group: '服务器资源' },
|
||||||
|
{ key: 'notify_alert_disk', label: '磁盘超阈值告警', desc: '磁盘使用率超过设定阈值时推送', group: '服务器资源' },
|
||||||
|
{ key: 'notify_recovery', label: '资源恢复正常', desc: 'CPU/内存/磁盘告警解除时推送', group: '服务器资源' },
|
||||||
|
{ key: 'notify_time_drift', label: '时钟偏差严重 (≥60s)', desc: '子机与主机时间偏差过大影响 TOTP/cron', group: '服务器' },
|
||||||
|
{ key: 'notify_system_redis', label: 'Redis 连接异常 / 恢复', desc: 'Redis 不可达或恢复时推送', group: '系统' },
|
||||||
|
{ key: 'notify_system_mysql', label: 'MySQL 连接异常 / 恢复', desc: 'MySQL 不可达或恢复时推送', group: '系统' },
|
||||||
|
{ key: 'notify_restart', label: 'Nexus 后端重启', desc: 'Python 服务崩溃重启成功/失败时推送', group: '系统' },
|
||||||
|
]
|
||||||
|
|
||||||
/** IP Allowlist response */
|
/** IP Allowlist response */
|
||||||
export interface AllowlistResponse {
|
export interface AllowlistResponse {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|||||||
+52
-14
@@ -291,14 +291,11 @@ async def get_setting(key: str, admin: Admin = Depends(get_current_admin), db: A
|
|||||||
|
|
||||||
@router.post("/api-key/reveal", response_model=dict)
|
@router.post("/api-key/reveal", response_model=dict)
|
||||||
async def reveal_api_key(
|
async def reveal_api_key(
|
||||||
payload: ApiKeyRevealRequest,
|
|
||||||
request: Request,
|
request: Request,
|
||||||
admin: Admin = Depends(get_current_admin),
|
admin: Admin = Depends(get_current_admin),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Reveal global API_KEY after re-entering account password."""
|
"""Reveal global API_KEY for authenticated admin (JWT required)."""
|
||||||
await _verify_reauth(db, admin, payload.current_password)
|
|
||||||
|
|
||||||
repo = SettingRepositoryImpl(db)
|
repo = SettingRepositoryImpl(db)
|
||||||
value = await repo.get("api_key")
|
value = await repo.get("api_key")
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -903,13 +900,34 @@ async def alert_stats(
|
|||||||
|
|
||||||
# ── Telegram Test + Chat ID Detection ──
|
# ── Telegram Test + Chat ID Detection ──
|
||||||
|
|
||||||
|
async def _telegram_clear_webhook_if_set(client, base_url: str) -> None:
|
||||||
|
"""Remove active webhook so getUpdates can receive messages."""
|
||||||
|
wh_resp = await client.get(f"{base_url}/getWebhookInfo")
|
||||||
|
if wh_resp.status_code != 200:
|
||||||
|
raise HTTPException(status_code=502, detail=f"Telegram getWebhookInfo 返回 {wh_resp.status_code}")
|
||||||
|
wh_data = wh_resp.json()
|
||||||
|
if not wh_data.get("ok"):
|
||||||
|
desc = wh_data.get("description") or "getWebhookInfo 失败"
|
||||||
|
raise HTTPException(status_code=502, detail=f"Telegram API: {desc}")
|
||||||
|
if wh_data.get("result", {}).get("url"):
|
||||||
|
del_resp = await client.post(
|
||||||
|
f"{base_url}/deleteWebhook",
|
||||||
|
params={"drop_pending_updates": "false"},
|
||||||
|
)
|
||||||
|
del_data = del_resp.json()
|
||||||
|
if not del_data.get("ok"):
|
||||||
|
desc = del_data.get("description") or "deleteWebhook 失败"
|
||||||
|
raise HTTPException(status_code=502, detail=f"无法清除 Webhook: {desc}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/telegram/test", response_model=dict)
|
@router.post("/telegram/test", response_model=dict)
|
||||||
async def telegram_test_send(
|
async def telegram_test_send(
|
||||||
admin: Admin = Depends(get_current_admin),
|
admin: Admin = Depends(get_current_admin),
|
||||||
):
|
):
|
||||||
"""Send a test Telegram message to verify Bot Token + Chat ID configuration."""
|
"""Send a test Telegram message to verify Bot Token + Chat ID configuration."""
|
||||||
from server.infrastructure.telegram import send_telegram
|
import httpx
|
||||||
from server.config import settings as _settings
|
from server.config import settings as _settings
|
||||||
|
from server.infrastructure.telegram import TELEGRAM_API_BASE
|
||||||
|
|
||||||
if not _settings.TELEGRAM_BOT_TOKEN:
|
if not _settings.TELEGRAM_BOT_TOKEN:
|
||||||
raise HTTPException(status_code=400, detail="TELEGRAM_BOT_TOKEN 未配置")
|
raise HTTPException(status_code=400, detail="TELEGRAM_BOT_TOKEN 未配置")
|
||||||
@@ -918,26 +936,41 @@ async def telegram_test_send(
|
|||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
ok = await send_telegram(
|
message = (
|
||||||
f"✅ <b>Nexus 测试消息</b>\n"
|
f"✅ <b>Nexus 测试消息</b>\n"
|
||||||
f"Bot 配置正确,告警通知已就绪\n"
|
f"Bot 配置正确,告警通知已就绪\n"
|
||||||
f"时间: {now}"
|
f"时间: {now}"
|
||||||
)
|
)
|
||||||
if ok:
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
resp = await client.post(
|
||||||
|
f"{TELEGRAM_API_BASE}/bot{_settings.TELEGRAM_BOT_TOKEN}/sendMessage",
|
||||||
|
json={
|
||||||
|
"chat_id": _settings.TELEGRAM_CHAT_ID,
|
||||||
|
"text": message,
|
||||||
|
"parse_mode": "HTML",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
data = resp.json()
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=502, detail=f"请求失败: {e}") from e
|
||||||
|
|
||||||
|
if resp.status_code == 200 and data.get("ok"):
|
||||||
return {"success": True}
|
return {"success": True}
|
||||||
raise HTTPException(status_code=502, detail="Telegram API 返回错误,请检查 Bot Token 和 Chat ID")
|
desc = data.get("description") or f"HTTP {resp.status_code}"
|
||||||
|
raise HTTPException(status_code=502, detail=f"Telegram API: {desc}")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/telegram/reveal-token", response_model=dict)
|
@router.post("/telegram/reveal-token", response_model=dict)
|
||||||
async def reveal_telegram_token(
|
async def reveal_telegram_token(
|
||||||
payload: ApiKeyRevealRequest,
|
|
||||||
request: Request,
|
request: Request,
|
||||||
admin: Admin = Depends(get_current_admin),
|
admin: Admin = Depends(get_current_admin),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Reveal Telegram Bot Token after re-entering account password."""
|
"""Reveal Telegram Bot Token for authenticated admin (JWT required)."""
|
||||||
await _verify_reauth(db, admin, payload.current_password)
|
|
||||||
|
|
||||||
repo = SettingRepositoryImpl(db)
|
repo = SettingRepositoryImpl(db)
|
||||||
value = await repo.get("telegram_bot_token")
|
value = await repo.get("telegram_bot_token")
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -973,13 +1006,18 @@ async def telegram_get_chats(
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
base_url = f"{TELEGRAM_API_BASE}/bot{token}"
|
||||||
|
await _telegram_clear_webhook_if_set(client, base_url)
|
||||||
resp = await client.get(
|
resp = await client.get(
|
||||||
f"{TELEGRAM_API_BASE}/bot{token}/getUpdates",
|
f"{base_url}/getUpdates",
|
||||||
params={"limit": 50, "allowed_updates": ["message", "channel_post"]},
|
params={"limit": 50},
|
||||||
)
|
)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
raise HTTPException(status_code=502, detail=f"Telegram API 返回 {resp.status_code}")
|
raise HTTPException(status_code=502, detail=f"Telegram API 返回 {resp.status_code}")
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
|
if not data.get("ok"):
|
||||||
|
desc = data.get("description") or "getUpdates 失败"
|
||||||
|
raise HTTPException(status_code=502, detail=f"Telegram API: {desc}")
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""Tests for Telegram settings endpoints and notify toggle hot-reload."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from server.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_telegram_test_400_when_token_missing():
|
||||||
|
from server.api.settings import telegram_test_send
|
||||||
|
|
||||||
|
settings.TELEGRAM_BOT_TOKEN = ""
|
||||||
|
settings.TELEGRAM_CHAT_ID = ""
|
||||||
|
admin = MagicMock()
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await telegram_test_send(admin=admin)
|
||||||
|
assert exc.value.status_code == 400
|
||||||
|
assert "TELEGRAM_BOT_TOKEN" in exc.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_telegram_test_400_when_chat_id_missing():
|
||||||
|
from server.api.settings import telegram_test_send
|
||||||
|
|
||||||
|
settings.TELEGRAM_BOT_TOKEN = "123456:ABC"
|
||||||
|
settings.TELEGRAM_CHAT_ID = ""
|
||||||
|
admin = MagicMock()
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await telegram_test_send(admin=admin)
|
||||||
|
assert exc.value.status_code == 400
|
||||||
|
assert "TELEGRAM_CHAT_ID" in exc.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_telegram_test_surfaces_api_error():
|
||||||
|
from server.api.settings import telegram_test_send
|
||||||
|
|
||||||
|
settings.TELEGRAM_BOT_TOKEN = "123456:ABC"
|
||||||
|
settings.TELEGRAM_CHAT_ID = "999"
|
||||||
|
admin = MagicMock()
|
||||||
|
|
||||||
|
mock_resp = MagicMock()
|
||||||
|
mock_resp.status_code = 400
|
||||||
|
mock_resp.json.return_value = {"ok": False, "description": "Bad Request: chat not found"}
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
mock_client.post = AsyncMock(return_value=mock_resp)
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await telegram_test_send(admin=admin)
|
||||||
|
|
||||||
|
assert exc.value.status_code == 502
|
||||||
|
assert "chat not found" in exc.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_telegram_chats_surfaces_api_error_when_ok_false():
|
||||||
|
from server.api.settings import telegram_get_chats
|
||||||
|
|
||||||
|
settings.TELEGRAM_BOT_TOKEN = "123456:ABC"
|
||||||
|
admin = MagicMock()
|
||||||
|
|
||||||
|
wh_resp = MagicMock()
|
||||||
|
wh_resp.status_code = 200
|
||||||
|
wh_resp.json.return_value = {"ok": True, "result": {"url": ""}}
|
||||||
|
|
||||||
|
updates_resp = MagicMock()
|
||||||
|
updates_resp.status_code = 200
|
||||||
|
updates_resp.json.return_value = {"ok": False, "description": "Unauthorized"}
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
mock_client.get = AsyncMock(side_effect=[wh_resp, updates_resp])
|
||||||
|
mock_client.post = AsyncMock()
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
await telegram_get_chats(admin=admin)
|
||||||
|
|
||||||
|
assert exc.value.status_code == 502
|
||||||
|
assert "Unauthorized" in exc.value.detail
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_telegram_chats_deletes_webhook_before_get_updates():
|
||||||
|
from server.api.settings import telegram_get_chats
|
||||||
|
|
||||||
|
settings.TELEGRAM_BOT_TOKEN = "123456:ABC"
|
||||||
|
admin = MagicMock()
|
||||||
|
|
||||||
|
wh_resp = MagicMock()
|
||||||
|
wh_resp.status_code = 200
|
||||||
|
wh_resp.json.return_value = {"ok": True, "result": {"url": "https://example.com/hook"}}
|
||||||
|
|
||||||
|
del_resp = MagicMock()
|
||||||
|
del_resp.json.return_value = {"ok": True}
|
||||||
|
|
||||||
|
updates_resp = MagicMock()
|
||||||
|
updates_resp.status_code = 200
|
||||||
|
updates_resp.json.return_value = {
|
||||||
|
"ok": True,
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"chat": {"id": 42, "type": "private", "first_name": "Test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
mock_client.__aexit__.return_value = None
|
||||||
|
mock_client.get = AsyncMock(side_effect=[wh_resp, updates_resp])
|
||||||
|
mock_client.post = AsyncMock(return_value=del_resp)
|
||||||
|
|
||||||
|
with patch("httpx.AsyncClient", return_value=mock_client):
|
||||||
|
result = await telegram_get_chats(admin=admin)
|
||||||
|
|
||||||
|
assert result["count"] == 1
|
||||||
|
assert result["chats"][0]["id"] == 42
|
||||||
|
mock_client.post.assert_called_once()
|
||||||
|
call_args = mock_client.post.call_args
|
||||||
|
assert "deleteWebhook" in call_args[0][0]
|
||||||
|
assert call_args[1]["params"]["drop_pending_updates"] == "false"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_notify_toggle_put_hot_reload():
|
||||||
|
from server.api.schemas import SettingUpdatePayload
|
||||||
|
from server.api.settings import set_setting
|
||||||
|
from server.domain.models import Setting
|
||||||
|
|
||||||
|
settings.NOTIFY_ALERT_CPU = "true"
|
||||||
|
admin = MagicMock(username="admin")
|
||||||
|
request = MagicMock()
|
||||||
|
request.client.host = "127.0.0.1"
|
||||||
|
db = MagicMock()
|
||||||
|
|
||||||
|
mock_setting = Setting(key="notify_alert_cpu", value="false")
|
||||||
|
|
||||||
|
with patch("server.api.settings.SettingRepositoryImpl") as repo_cls, patch(
|
||||||
|
"server.api.settings.AuditLogRepositoryImpl"
|
||||||
|
) as audit_cls:
|
||||||
|
repo = repo_cls.return_value
|
||||||
|
repo.set = AsyncMock(return_value=mock_setting)
|
||||||
|
audit_cls.return_value.create = AsyncMock()
|
||||||
|
|
||||||
|
await set_setting(
|
||||||
|
"notify_alert_cpu",
|
||||||
|
SettingUpdatePayload(value="false"),
|
||||||
|
request,
|
||||||
|
admin,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert settings.NOTIFY_ALERT_CPU == "false"
|
||||||
Reference in New Issue
Block a user