diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 8108987..8c0f107 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -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 diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index a0c93b1..383915f 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -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 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 931ec07..5fffcfa 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -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 diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 6abfc6e..7eb88ea 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -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 diff --git a/docs/LOG/LOG-00009-CI-CD门禁加固-执行日志.md b/docs/LOG/LOG-00009-CI-CD门禁加固-执行日志.md new file mode 100644 index 0000000..0f26eea --- /dev/null +++ b/docs/LOG/LOG-00009-CI-CD门禁加固-执行日志.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 2bdc945..912ecbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/sonar-project.properties b/sonar-project.properties index e82883d..f078a0f 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -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 diff --git a/tests/test_external_api.py b/tests/test_external_api.py index 91f723c..9920b71 100644 --- a/tests/test_external_api.py +++ b/tests/test_external_api.py @@ -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] diff --git a/tests/test_ui_redesign_bugs.py b/tests/test_ui_redesign_bugs.py index feb8776..75ed48e 100644 --- a/tests/test_ui_redesign_bugs.py +++ b/tests/test_ui_redesign_bugs.py @@ -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) diff --git a/tests/test_ui_settings_external_api_key.py b/tests/test_ui_settings_external_api_key.py index 9e4ec20..31a84d3 100644 --- a/tests/test_ui_settings_external_api_key.py +++ b/tests/test_ui_settings_external_api_key.py @@ -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)