ci: harden quality gates and clean test warnings
This commit is contained in:
1 parent
1d4d848702
commit
bd9bafeb83
10 files changed
+359
-63
No files matched your search
@@ -5,11 +5,13 @@ on:
|
||||
branches: [ "main", "master" ]
|
||||
paths:
|
||||
- "**.py"
|
||||
- "pyproject.toml"
|
||||
- "requirements.txt"
|
||||
- ".github/workflows/code-quality.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.py"
|
||||
- "pyproject.toml"
|
||||
- "requirements.txt"
|
||||
- ".github/workflows/code-quality.yml"
|
||||
workflow_dispatch:
|
||||
@@ -30,19 +32,38 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install flake8 black isort mypy
|
||||
|
||||
- name: Run Black (Code Formatter Check)
|
||||
run: black --check --diff .
|
||||
run: black --check --diff outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py
|
||||
|
||||
- name: Run isort (Import Sorting Check)
|
||||
run: isort --check-only --diff --profile black .
|
||||
run: isort --check-only --diff --profile black outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py
|
||||
|
||||
- name: Run Flake8 (Linting)
|
||||
run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
run: flake8 outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
|
||||
- name: Run Flake8 (Complexity Check)
|
||||
run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
run: >
|
||||
flake8
|
||||
outlook_web/repositories/settings.py
|
||||
outlook_web/services/external_api.py
|
||||
outlook_web/controllers/system.py
|
||||
web_outlook_app.py
|
||||
--count
|
||||
--max-complexity=10
|
||||
--max-line-length=127
|
||||
--statistics
|
||||
|
||||
- name: Run mypy (Core Modules)
|
||||
run: >
|
||||
mypy
|
||||
--config-file pyproject.toml
|
||||
outlook_web/repositories/settings.py
|
||||
outlook_web/services/external_api.py
|
||||
outlook_web/controllers/system.py
|
||||
web_outlook_app.py
|
||||
|
||||
security:
|
||||
name: Security Scan
|
||||
@@ -63,13 +84,33 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install bandit[toml]
|
||||
|
||||
- name: Run Bandit Security Scan
|
||||
run: bandit -r . -f json -o bandit-report.json || true
|
||||
|
||||
- name: Display Bandit Results
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -f bandit-report.json ]; then
|
||||
cat bandit-report.json
|
||||
set +e
|
||||
bandit -r outlook_web web_outlook_app.py outlook_mail_reader.py start.py -f sarif -o bandit-report.sarif
|
||||
report_status=$?
|
||||
bandit -r outlook_web web_outlook_app.py outlook_mail_reader.py start.py -lll
|
||||
gate_status=$?
|
||||
echo "bandit_report_exit_code=$report_status" >> "$GITHUB_ENV"
|
||||
echo "bandit_gate_exit_code=$gate_status" >> "$GITHUB_ENV"
|
||||
exit 0
|
||||
|
||||
- name: Upload Bandit SARIF
|
||||
if: always() && hashFiles('bandit-report.sarif') != ''
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: bandit-report.sarif
|
||||
|
||||
- name: Fail on Bandit Findings
|
||||
if: always()
|
||||
shell: bash
|
||||
run: |
|
||||
status="${bandit_gate_exit_code:-1}"
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "Bandit found high-severity issues, exit code=$status"
|
||||
exit "$status"
|
||||
fi
|
||||
@@ -7,9 +7,11 @@ on:
|
||||
- master
|
||||
paths:
|
||||
- '**.py'
|
||||
- 'pyproject.toml'
|
||||
- 'requirements.txt'
|
||||
- 'Dockerfile'
|
||||
- 'templates/**'
|
||||
- 'static/**'
|
||||
- '.github/workflows/docker-build-push.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -18,7 +20,43 @@ env:
|
||||
DOCKERHUB_IMAGE: guangshanshui/outlook-email-plus
|
||||
|
||||
jobs:
|
||||
quality-gate:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install flake8 black isort mypy bandit[toml]
|
||||
|
||||
- name: Run formatter checks
|
||||
run: |
|
||||
black --check outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py
|
||||
isort --check-only --profile black outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py
|
||||
|
||||
- name: Run static analysis gate
|
||||
run: |
|
||||
flake8 outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
flake8 outlook_web/repositories/settings.py outlook_web/services/external_api.py outlook_web/controllers/system.py web_outlook_app.py --count --max-complexity=10 --max-line-length=127 --statistics
|
||||
mypy --config-file pyproject.toml outlook_web/repositories/settings.py outlook_web/services/external_api.py outlook_web/controllers/system.py web_outlook_app.py
|
||||
|
||||
- name: Run security gate
|
||||
run: bandit -r outlook_web web_outlook_app.py outlook_mail_reader.py start.py -lll
|
||||
|
||||
- name: Run publish gate tests
|
||||
run: python -m unittest discover -s tests -v
|
||||
|
||||
build-and-push:
|
||||
needs: quality-gate
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -5,14 +5,20 @@ on:
|
||||
branches: [ "main", "master" ]
|
||||
paths:
|
||||
- "**.py"
|
||||
- "pyproject.toml"
|
||||
- "requirements.txt"
|
||||
- "tests/**"
|
||||
- "templates/**"
|
||||
- "static/**"
|
||||
- ".github/workflows/python-tests.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "**.py"
|
||||
- "pyproject.toml"
|
||||
- "requirements.txt"
|
||||
- "tests/**"
|
||||
- "templates/**"
|
||||
- "static/**"
|
||||
- ".github/workflows/python-tests.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -37,12 +43,21 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install coverage
|
||||
|
||||
- name: Syntax check
|
||||
run: |
|
||||
python -m py_compile web_outlook_app.py
|
||||
python -m py_compile outlook_mail_reader.py
|
||||
python -m compileall -q outlook_web web_outlook_app.py outlook_mail_reader.py start.py tests
|
||||
|
||||
- name: Run unit tests
|
||||
run: python -m unittest discover -s tests -v
|
||||
- name: Run unit tests with coverage
|
||||
run: |
|
||||
coverage run -m unittest discover -s tests -v
|
||||
coverage xml
|
||||
coverage report
|
||||
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-python-${{ matrix.python-version }}
|
||||
path: coverage.xml
|
||||
|
||||
@@ -31,6 +31,12 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements.txt
|
||||
python -m pip install coverage
|
||||
|
||||
- name: Run tests with coverage
|
||||
run: |
|
||||
coverage run -m unittest discover -s tests -v
|
||||
coverage xml
|
||||
|
||||
- name: SonarQube Scan
|
||||
uses: sonarsource/sonarqube-scan-action@v5
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# LOG-00009|CI/CD 门禁加固 — 执行日志
|
||||
|
||||
## 目标
|
||||
|
||||
- 解决当前 CI/CD “有流程但门禁不硬”的问题
|
||||
- 降低坏代码进入镜像仓库与主分支的概率
|
||||
- 补齐类型检查、覆盖率与安全扫描的实际执行链路
|
||||
|
||||
## 问题分析
|
||||
|
||||
### 1) Docker 发布未绑定质量门禁
|
||||
|
||||
- 现状:
|
||||
- Docker workflow 只要推送到 `main/master` 就会执行构建并推送镜像
|
||||
- 与 Python Tests / Code Quality workflow 没有依赖关系
|
||||
- 风险:
|
||||
- 单元测试失败时,镜像仍可能被推送到 GHCR / Docker Hub
|
||||
|
||||
### 2) 安全扫描不阻断
|
||||
|
||||
- 现状:
|
||||
- Bandit 使用 `|| true` 吞掉退出码
|
||||
- 只输出 JSON,不上传 SARIF,也不作为失败门禁
|
||||
- 风险:
|
||||
- 安全问题只会显示在日志中,不会阻止合并或发布
|
||||
|
||||
### 3) `mypy` 名义存在但未执行
|
||||
|
||||
- 现状:
|
||||
- Workflow 安装了 `mypy`
|
||||
- 但没有任何实际 `mypy` 步骤
|
||||
- 风险:
|
||||
- 类型回归无法在 CI 中被发现
|
||||
|
||||
### 4) Sonar 无覆盖率输入
|
||||
|
||||
- 现状:
|
||||
- Sonar workflow 只安装依赖并扫描
|
||||
- 未生成 `coverage.xml`
|
||||
- `sonar-project.properties` 未声明 Python coverage report
|
||||
- 风险:
|
||||
- Sonar 只能做静态分析,无法给出真实测试覆盖信号
|
||||
|
||||
### 5) 路径过滤过窄
|
||||
|
||||
- 现状:
|
||||
- `static/**`、`templates/**`、`pyproject.toml` 等变更在部分 workflow 下不会触发
|
||||
- 风险:
|
||||
- 实际影响镜像内容或检查规则的改动,可能绕过对应流水线
|
||||
|
||||
## 本轮整改(2026-03-10)
|
||||
|
||||
### A. Python Tests
|
||||
|
||||
- 补充触发路径:
|
||||
- `pyproject.toml`
|
||||
- `templates/**`
|
||||
- `static/**`
|
||||
- 将语法检查从仅入口文件升级为:
|
||||
- `compileall` 检查 `outlook_web/`、入口文件与 `tests/`
|
||||
- 新增 coverage 执行:
|
||||
- `coverage run -m unittest discover -s tests -v`
|
||||
- `coverage xml`
|
||||
- `coverage report`
|
||||
- 上传 `coverage.xml` 作为 artifact,便于后续排查
|
||||
|
||||
### B. Code Quality
|
||||
|
||||
- 质量检查范围收敛到真实项目代码,避免把 `.venv/`、示例目录等一起扫进去
|
||||
- `black` / `isort` / `flake8` 统一针对:
|
||||
- `outlook_web`
|
||||
- `tests`
|
||||
- `web_outlook_app.py`
|
||||
- `outlook_mail_reader.py`
|
||||
- `start.py`
|
||||
- 复杂度阻断首期仅覆盖:
|
||||
- `repositories/settings.py`
|
||||
- `services/external_api.py`
|
||||
- `controllers/system.py`
|
||||
- `web_outlook_app.py`
|
||||
- 增加 `mypy` 实际执行,首期先覆盖核心模块:
|
||||
- `repositories/settings.py`
|
||||
- `services/external_api.py`
|
||||
- `controllers/system.py`
|
||||
- `web_outlook_app.py`
|
||||
- 在 `pyproject.toml` 增加基础 mypy 配置:
|
||||
- `ignore_missing_imports = true`
|
||||
- `warn_unused_ignores = true`
|
||||
- `follow_imports = "skip"`
|
||||
- 说明:
|
||||
- 当前仓库历史类型债较重,首期采用“核心模块 + skip imports”方式渐进接入
|
||||
- 暂不把 `graph.py`、`emails.py` 等类型债较重模块纳入阻断集合
|
||||
|
||||
### C. Security Scan
|
||||
|
||||
- Bandit 改为扫描主代码入口,而不是整个仓库
|
||||
- 输出格式改为 `sarif`
|
||||
- 新增 SARIF 上传到 GitHub Security
|
||||
- 不再直接完全放行;改为:
|
||||
1. 先运行 Bandit 并保存退出码
|
||||
2. 始终上传 SARIF
|
||||
3. 额外执行高危门禁扫描
|
||||
4. 仅在高严重度问题出现时阻断 workflow
|
||||
- 说明:
|
||||
- 当前仓库已有较多历史低/中危告警,首期先将高危问题设为阻断条件
|
||||
|
||||
### D. Docker Build Push
|
||||
|
||||
- 增加 `quality-gate` job
|
||||
- `build-and-push` 必须 `needs: quality-gate`
|
||||
- 发布前现在保证:
|
||||
- `black` / `isort` 格式门禁通过
|
||||
- `flake8` 语法与首批复杂度门禁通过
|
||||
- `mypy` 核心模块类型检查通过
|
||||
- `bandit -lll` 高危安全门禁通过
|
||||
- `python -m unittest discover -s tests -v` 通过
|
||||
- 同时补充触发路径:
|
||||
- `pyproject.toml`
|
||||
- `static/**`
|
||||
|
||||
### E. Sonar
|
||||
|
||||
- Sonar workflow 在扫描前先执行:
|
||||
- `coverage run -m unittest discover -s tests -v`
|
||||
- `coverage xml`
|
||||
- `sonar-project.properties` 增加:
|
||||
- `sonar.python.coverage.reportPaths=coverage.xml`
|
||||
|
||||
### F. 测试日志清理
|
||||
|
||||
- 修复以下测试文件中多个响应对象未及时关闭的问题:
|
||||
- `tests/test_ui_redesign_bugs.py`
|
||||
- `tests/test_ui_settings_external_api_key.py`
|
||||
- 目标:
|
||||
- 清除 CI 中 `ResourceWarning: unclosed file` 噪音
|
||||
- 让测试日志更干净,便于定位真实失败原因
|
||||
|
||||
## 本轮尚未彻底解决的点
|
||||
|
||||
### 1) Docker 发布仍存在“检查重复执行”
|
||||
|
||||
- 当前做法:
|
||||
- 通过在 Docker workflow 内重复执行核心质量检查与测试来保证发布门禁
|
||||
- 原因:
|
||||
- GitHub Actions 原生 `workflow_run` 不适合同时精确依赖多个独立 workflow 的成功结果
|
||||
- 后续优化方向:
|
||||
- 进一步抽象为 reusable workflow 或单一主流水线
|
||||
|
||||
### 2) `mypy` 仍是“核心模块优先”,不是全仓强校验
|
||||
|
||||
- 当前策略是渐进式引入,避免一次性把类型债全部打爆
|
||||
- 后续应逐步扩大到更多模块
|
||||
|
||||
### 3) Bandit 当前仅对高危问题阻断
|
||||
|
||||
- 低/中危问题会进入 SARIF 与日志,但不会在首期直接打断 CI
|
||||
- 后续应结合告警治理逐步收紧到中危
|
||||
|
||||
## 建议验证步骤
|
||||
|
||||
```bash
|
||||
python -m black --check outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py
|
||||
python -m isort --check-only --profile black outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py
|
||||
python -m flake8 outlook_web tests web_outlook_app.py outlook_mail_reader.py start.py --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
python -m flake8 outlook_web/repositories/settings.py outlook_web/services/external_api.py outlook_web/controllers/system.py web_outlook_app.py --count --max-complexity=10 --max-line-length=127 --statistics
|
||||
python -m mypy --config-file pyproject.toml outlook_web/repositories/settings.py outlook_web/services/external_api.py outlook_web/controllers/system.py web_outlook_app.py
|
||||
python -m bandit -r outlook_web web_outlook_app.py outlook_mail_reader.py start.py -lll
|
||||
python -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
## 结论
|
||||
|
||||
本轮整改后,CI/CD 从“有工作流”提升到了“关键路径开始具备真实门禁”:
|
||||
|
||||
- 发布前至少会跑测试
|
||||
- 安全扫描不再默默放行
|
||||
- 类型检查开始进入 CI
|
||||
- 覆盖率开始进入流水线
|
||||
|
||||
仍然建议后续继续推进:
|
||||
|
||||
1. 合并为单一主流水线或 reusable workflow
|
||||
2. 扩大 mypy 覆盖范围
|
||||
3. 完整接入 Sonar coverage report
|
||||
@@ -4,3 +4,11 @@ line-length = 127
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 127
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
ignore_missing_imports = true
|
||||
warn_unused_ignores = true
|
||||
show_error_codes = true
|
||||
pretty = true
|
||||
follow_imports = "skip"
|
||||
@@ -19,3 +19,4 @@ sonar.exclusions=.git/**,.venv/**,venv/**,__pycache__/**,**/__pycache__/**,.pyte
|
||||
# Exclude non-production and generated files from duplication and coverage calculations.
|
||||
sonar.cpd.exclusions=docs/**,img/**,dist/**,exeample/**,tests/**,tests/layout-system/coverage/**
|
||||
sonar.coverage.exclusions=tests/**,templates/**,static/**,docs/**,img/**,dist/**,data/**,exeample/**,web_outlook_app.py,start.py,fix_format.py,outlook_mail_reader.py
|
||||
sonar.python.coverage.reportPaths=coverage.xml
|
||||
@@ -119,14 +119,12 @@ class ExternalApiBaseTest(unittest.TestCase):
|
||||
from outlook_web.db import get_db
|
||||
|
||||
db = get_db()
|
||||
rows = db.execute(
|
||||
"""
|
||||
rows = db.execute("""
|
||||
SELECT action, resource_id, details
|
||||
FROM audit_logs
|
||||
WHERE resource_type = 'external_api'
|
||||
ORDER BY id ASC
|
||||
"""
|
||||
).fetchall()
|
||||
""").fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
|
||||
@@ -34,13 +34,28 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
self._login(client)
|
||||
return client
|
||||
|
||||
def _get_text(self, client, path):
|
||||
"""Read response text and close the underlying file handle promptly."""
|
||||
resp = client.get(path)
|
||||
try:
|
||||
return resp.status_code, resp.data.decode("utf-8")
|
||||
finally:
|
||||
resp.close()
|
||||
|
||||
def _get_json(self, client, path):
|
||||
"""Read JSON payload and close the response object."""
|
||||
resp = client.get(path)
|
||||
try:
|
||||
return resp.status_code, resp.get_json()
|
||||
finally:
|
||||
resp.close()
|
||||
|
||||
# ==================== BUG-002: 账号栏显示 ====================
|
||||
|
||||
def test_bug002_current_account_bar_exists(self):
|
||||
"""BUG-002: index.html 中存在 currentAccountBar 元素"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/")
|
||||
html = resp.data.decode("utf-8")
|
||||
_, html = self._get_text(client, "/")
|
||||
self.assertIn('id="currentAccountBar"', html)
|
||||
self.assertIn('id="currentAccountEmail"', html)
|
||||
self.assertIn('id="currentAccount"', html)
|
||||
@@ -48,8 +63,7 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_bug002_accounts_js_shows_account_bar(self):
|
||||
"""BUG-002: accounts.js 中 selectAccount 应操作 currentAccountBar 而非 currentAccount"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/static/js/features/accounts.js")
|
||||
js = resp.data.decode("utf-8")
|
||||
_, js = self._get_text(client, "/static/js/features/accounts.js")
|
||||
self.assertIn("currentAccountBar", js, "selectAccount() 应操作 #currentAccountBar 元素")
|
||||
|
||||
# ==================== BUG-003: 设置页面 ====================
|
||||
@@ -57,16 +71,14 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_bug003_settings_page_has_content(self):
|
||||
"""BUG-003: 设置页面应有实质性内容,而非仅占位文字"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/")
|
||||
html = resp.data.decode("utf-8")
|
||||
_, html = self._get_text(client, "/")
|
||||
settings_section = re.search(r'id="page-settings".*?(?=id="page-|$)', html, re.DOTALL)
|
||||
self.assertIsNotNone(settings_section, "找不到 page-settings 区域")
|
||||
|
||||
def test_bug003_navigate_triggers_settings_load(self):
|
||||
"""BUG-003: navigate('settings') 应触发设置加载"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/static/js/main.js")
|
||||
js = resp.data.decode("utf-8")
|
||||
_, js = self._get_text(client, "/static/js/main.js")
|
||||
self.assertIn("settings", js)
|
||||
|
||||
# ==================== BUG-004: 弹窗居中 ====================
|
||||
@@ -74,23 +86,20 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_bug004_modal_css_centering(self):
|
||||
"""BUG-004: CSS 中 .modal 应有 flex 居中属性"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/static/css/main.css")
|
||||
css = resp.data.decode("utf-8")
|
||||
_, css = self._get_text(client, "/static/css/main.css")
|
||||
self.assertIn("align-items: center", css)
|
||||
self.assertIn("justify-content: center", css)
|
||||
|
||||
def test_bug004_modal_content_max_width(self):
|
||||
"""BUG-004: .modal-content 应有 max-width 限制"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/static/css/main.css")
|
||||
css = resp.data.decode("utf-8")
|
||||
_, css = self._get_text(client, "/static/css/main.css")
|
||||
self.assertIn("max-width: 560px", css)
|
||||
|
||||
def test_bug004_all_modals_use_modal_class(self):
|
||||
"""BUG-004: modals.html 中所有弹窗应使用 class='modal'"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/")
|
||||
html = resp.data.decode("utf-8")
|
||||
_, html = self._get_text(client, "/")
|
||||
modal_ids = re.findall(r'id="(\w+Modal)"', html)
|
||||
for modal_id in modal_ids:
|
||||
if modal_id == "fullscreenEmailModal":
|
||||
@@ -110,14 +119,13 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_bug005_verification_extract_api_exists(self):
|
||||
"""BUG-005: 验证码提取 API 端点存在"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/api/emails/nonexistent@test.com/extract-verification")
|
||||
self.assertIn(resp.status_code, [200, 404], "验证码提取 API 应该返回 200 或 404")
|
||||
status_code, _ = self._get_json(client, "/api/emails/nonexistent@test.com/extract-verification")
|
||||
self.assertIn(status_code, [200, 404], "验证码提取 API 应该返回 200 或 404")
|
||||
|
||||
def test_bug005_copy_verification_function_exists(self):
|
||||
"""BUG-005: JS 中存在 copyVerificationInfo 函数"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/static/js/features/groups.js")
|
||||
js = resp.data.decode("utf-8")
|
||||
_, js = self._get_text(client, "/static/js/features/groups.js")
|
||||
self.assertIn("function copyVerificationInfo", js)
|
||||
|
||||
# ==================== BUG-006: 卡片颜色 ====================
|
||||
@@ -125,8 +133,7 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_bug006_card_color_array_exists(self):
|
||||
"""BUG-006: 临时邮箱渲染应有多种颜色"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/static/js/features/temp_emails.js")
|
||||
js = resp.data.decode("utf-8")
|
||||
_, js = self._get_text(client, "/static/js/features/temp_emails.js")
|
||||
self.assertIn("renderTempEmailList", js)
|
||||
|
||||
# ==================== BUG-010: 仪表盘 ====================
|
||||
@@ -134,8 +141,7 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_bug010_dashboard_elements_exist(self):
|
||||
"""BUG-010: 仪表盘统计元素存在"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/")
|
||||
html = resp.data.decode("utf-8")
|
||||
_, html = self._get_text(client, "/")
|
||||
for el_id in [
|
||||
"statTotalAccounts",
|
||||
"statValidTokens",
|
||||
@@ -147,23 +153,20 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_bug010_dashboard_group_list_exists(self):
|
||||
"""BUG-010: 仪表盘分组概览列表存在"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/")
|
||||
html = resp.data.decode("utf-8")
|
||||
_, html = self._get_text(client, "/")
|
||||
self.assertIn('id="dashboardGroupList"', html)
|
||||
|
||||
def test_bug010_load_dashboard_function(self):
|
||||
"""BUG-010: loadDashboard 函数存在且调用正确 API"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/static/js/main.js")
|
||||
js = resp.data.decode("utf-8")
|
||||
_, js = self._get_text(client, "/static/js/main.js")
|
||||
self.assertIn("function loadDashboard", js)
|
||||
self.assertIn("/api/groups", js)
|
||||
|
||||
def test_bug010_groups_api_returns_data(self):
|
||||
"""BUG-010: 分组 API 返回有效数据"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/api/groups")
|
||||
data = resp.get_json()
|
||||
_, data = self._get_json(client, "/api/groups")
|
||||
self.assertTrue(data.get("success"), "分组 API 应返回 success=true")
|
||||
self.assertIn("groups", data, "分组 API 应包含 groups 字段")
|
||||
|
||||
@@ -172,8 +175,7 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_all_pages_exist_in_html(self):
|
||||
"""所有页面容器存在于 index.html"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/")
|
||||
html = resp.data.decode("utf-8")
|
||||
_, html = self._get_text(client, "/")
|
||||
pages = [
|
||||
"page-dashboard",
|
||||
"page-mailbox",
|
||||
@@ -188,8 +190,7 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_sidebar_navigation_items(self):
|
||||
"""侧边栏导航项完整"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/")
|
||||
html = resp.data.decode("utf-8")
|
||||
_, html = self._get_text(client, "/")
|
||||
nav_pages = [
|
||||
"dashboard",
|
||||
"mailbox",
|
||||
@@ -204,8 +205,7 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
def test_mailbox_three_column_structure(self):
|
||||
"""邮箱管理三栏布局结构完整"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/")
|
||||
html = resp.data.decode("utf-8")
|
||||
_, html = self._get_text(client, "/")
|
||||
self.assertIn('class="mailbox-layout"', html)
|
||||
self.assertIn('class="groups-column"', html)
|
||||
self.assertIn('class="accounts-column"', html)
|
||||
@@ -222,16 +222,15 @@ class TestUIRedesignBugFixes(unittest.TestCase):
|
||||
"/static/js/features/temp_emails.js",
|
||||
]
|
||||
for js_path in js_files:
|
||||
resp = client.get(js_path)
|
||||
self.assertEqual(resp.status_code, 200, f"{js_path} 应返回 200")
|
||||
self.assertGreater(len(resp.data), 100, f"{js_path} 内容不应为空")
|
||||
status_code, js = self._get_text(client, js_path)
|
||||
self.assertEqual(status_code, 200, f"{js_path} 应返回 200")
|
||||
self.assertGreater(len(js), 100, f"{js_path} 内容不应为空")
|
||||
|
||||
def test_css_loads_successfully(self):
|
||||
"""CSS 文件可正常加载"""
|
||||
client = self._get_client()
|
||||
resp = client.get("/static/css/main.css")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
css = resp.data.decode("utf-8")
|
||||
status_code, css = self._get_text(client, "/static/css/main.css")
|
||||
self.assertEqual(status_code, 200)
|
||||
self.assertIn("--clr-primary", css)
|
||||
self.assertIn("--bg-sidebar", css)
|
||||
self.assertIn("--bg-hover", css)
|
||||
|
||||
@@ -13,23 +13,29 @@ class ExternalApiKeySettingsUITests(unittest.TestCase):
|
||||
resp = client.post("/login", json={"password": password})
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(resp.get_json().get("success"))
|
||||
resp.close()
|
||||
|
||||
def _get_text(self, client, path):
|
||||
resp = client.get(path)
|
||||
try:
|
||||
return resp.status_code, resp.data.decode("utf-8")
|
||||
finally:
|
||||
resp.close()
|
||||
|
||||
def test_settings_page_has_external_api_key_input(self):
|
||||
client = self.app.test_client()
|
||||
self._login(client)
|
||||
|
||||
resp = client.get("/")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
html = resp.data.decode("utf-8")
|
||||
status_code, html = self._get_text(client, "/")
|
||||
self.assertEqual(status_code, 200)
|
||||
self.assertIn('id="settingsExternalApiKey"', html)
|
||||
|
||||
def test_main_js_loads_masked_external_api_key_fields(self):
|
||||
client = self.app.test_client()
|
||||
self._login(client)
|
||||
|
||||
resp = client.get("/static/js/main.js")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
js = resp.data.decode("utf-8")
|
||||
status_code, js = self._get_text(client, "/static/js/main.js")
|
||||
self.assertEqual(status_code, 200)
|
||||
self.assertIn("external_api_key_masked", js)
|
||||
self.assertIn("settingsExternalApiKey", js)
|
||||
self.assertIn("dataset.maskedValue", js)
|
||||
|
||||
Reference in new issue
Block a user