diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index ffb402c..8108987 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 @@ -36,7 +36,7 @@ jobs: run: black --check --diff . - name: Run isort (Import Sorting Check) - run: isort --check-only --diff . + run: isort --check-only --diff --profile black . - name: Run Flake8 (Linting) run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics @@ -53,7 +53,7 @@ jobs: actions: read steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index fd265fa..438ec36 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 9e350e8..931ec07 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python uses: actions/setup-python@v5 diff --git a/outlook_mail_reader.py b/outlook_mail_reader.py index 5a69ce6..bd351d9 100644 --- a/outlook_mail_reader.py +++ b/outlook_mail_reader.py @@ -11,7 +11,7 @@ Outlook 邮件读取测试工具 import email import imaplib from email.header import decode_header -from typing import Optional, List, Dict, Any +from typing import Any, Dict, List, Optional import requests diff --git a/outlook_web/app.py b/outlook_web/app.py index 7fbbe18..5d0e173 100644 --- a/outlook_web/app.py +++ b/outlook_web/app.py @@ -18,17 +18,16 @@ def create_app(*, autostart_scheduler: Optional[bool] = None): from pathlib import Path from flask import Flask - from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.exceptions import HTTPException + from werkzeug.middleware.proxy_fix import ProxyFix from outlook_web import config - from outlook_web.db import register_db, init_db - from outlook_web.security.csrf import init_csrf + from outlook_web.db import init_db, register_db from outlook_web.middleware import ( - ensure_trace_id, attach_trace_id_and_normalize_errors, - handle_http_exception, + ensure_trace_id, handle_exception, + handle_http_exception, ) from outlook_web.routes import ( accounts, @@ -43,6 +42,7 @@ def create_app(*, autostart_scheduler: Optional[bool] = None): tags, temp_emails, ) + from outlook_web.security.csrf import init_csrf # 初始化(DB/目录等) repo_root = Path(__file__).resolve().parents[1] @@ -128,16 +128,16 @@ def create_app(*, autostart_scheduler: Optional[bool] = None): # 调度器启动控制 if autostart_scheduler is None: - from outlook_web.services import scheduler as scheduler_service from outlook_web.services import graph as graph_service + from outlook_web.services import scheduler as scheduler_service if scheduler_service.should_autostart_scheduler(): scheduler_service.init_scheduler( _APP_INSTANCE, graph_service.test_refresh_token ) elif autostart_scheduler: - from outlook_web.services import scheduler as scheduler_service from outlook_web.services import graph as graph_service + from outlook_web.services import scheduler as scheduler_service scheduler_service.init_scheduler( _APP_INSTANCE, graph_service.test_refresh_token diff --git a/outlook_web/controllers/accounts.py b/outlook_web/controllers/accounts.py index 217dd9d..79cffa8 100644 --- a/outlook_web/controllers/accounts.py +++ b/outlook_web/controllers/accounts.py @@ -15,14 +15,14 @@ from outlook_web.db import get_db from outlook_web.errors import build_error_payload from outlook_web.repositories import accounts as accounts_repo from outlook_web.repositories import groups as groups_repo -from outlook_web.repositories import tags as tags_repo from outlook_web.repositories import refresh_logs as refresh_logs_repo from outlook_web.repositories import settings as settings_repo -from outlook_web.repositories.refresh_runs import create_refresh_run, finish_refresh_run +from outlook_web.repositories import tags as tags_repo from outlook_web.repositories.distributed_locks import ( acquire_distributed_lock, release_distributed_lock, ) +from outlook_web.repositories.refresh_runs import create_refresh_run, finish_refresh_run from outlook_web.security.auth import get_client_ip, get_user_agent, login_required from outlook_web.security.crypto import decrypt_data from outlook_web.services import graph as graph_service @@ -835,12 +835,12 @@ def api_export_selected_accounts() -> Any: @login_required def api_generate_export_verify_token() -> Any: """生成导出验证token(二次验证)""" + from outlook_web.repositories import settings as settings_repo from outlook_web.security.auth import ( - issue_export_verify_token, get_client_ip, get_user_agent, + issue_export_verify_token, ) - from outlook_web.repositories import settings as settings_repo from outlook_web.security.crypto import verify_password data = request.json diff --git a/outlook_web/controllers/audit.py b/outlook_web/controllers/audit.py index ff6d3bc..6e20726 100644 --- a/outlook_web/controllers/audit.py +++ b/outlook_web/controllers/audit.py @@ -7,7 +7,6 @@ from flask import jsonify, request from outlook_web.audit import query_audit_logs from outlook_web.security.auth import login_required - # ==================== 审计日志 API ==================== diff --git a/outlook_web/controllers/emails.py b/outlook_web/controllers/emails.py index 719e926..912f8c5 100644 --- a/outlook_web/controllers/emails.py +++ b/outlook_web/controllers/emails.py @@ -15,7 +15,6 @@ from outlook_web.services import email_delete as email_delete_service from outlook_web.services import graph as graph_service from outlook_web.services import imap as imap_service - # IMAP 服务器配置 IMAP_SERVER_OLD = "outlook.office365.com" IMAP_SERVER_NEW = "outlook.live.com" diff --git a/outlook_web/controllers/groups.py b/outlook_web/controllers/groups.py index 7a9ba46..9a87b77 100644 --- a/outlook_web/controllers/groups.py +++ b/outlook_web/controllers/groups.py @@ -8,6 +8,7 @@ from urllib.parse import quote from flask import Response, jsonify, request +from outlook_web.audit import log_audit from outlook_web.errors import build_error_payload from outlook_web.repositories import accounts as accounts_repo from outlook_web.repositories import groups as groups_repo @@ -18,7 +19,6 @@ from outlook_web.security.auth import ( get_user_agent, login_required, ) -from outlook_web.audit import log_audit def sanitize_input(text: str, max_length: int = 500) -> str: diff --git a/outlook_web/controllers/oauth.py b/outlook_web/controllers/oauth.py index 912dd18..e7e3cf3 100644 --- a/outlook_web/controllers/oauth.py +++ b/outlook_web/controllers/oauth.py @@ -14,7 +14,6 @@ from outlook_web.security.auth import ( login_required, ) - # OAuth 配置 OAUTH_SCOPES = [ "offline_access", diff --git a/outlook_web/controllers/pages.py b/outlook_web/controllers/pages.py index 93e11bb..7bb1507 100644 --- a/outlook_web/controllers/pages.py +++ b/outlook_web/controllers/pages.py @@ -15,7 +15,6 @@ from outlook_web.security.auth import ( ) from outlook_web.security.crypto import verify_password - # ==================== 页面路由 ==================== diff --git a/outlook_web/controllers/scheduler.py b/outlook_web/controllers/scheduler.py index 9ecce43..45b9280 100644 --- a/outlook_web/controllers/scheduler.py +++ b/outlook_web/controllers/scheduler.py @@ -12,7 +12,6 @@ from outlook_web.db import create_sqlite_connection from outlook_web.repositories import settings as settings_repo from outlook_web.security.auth import login_required - # 常量 REFRESH_LOCK_NAME = "token_refresh" diff --git a/outlook_web/controllers/settings.py b/outlook_web/controllers/settings.py index e90d522..c23d23c 100644 --- a/outlook_web/controllers/settings.py +++ b/outlook_web/controllers/settings.py @@ -11,7 +11,6 @@ from outlook_web.repositories import settings as settings_repo from outlook_web.security.auth import login_required from outlook_web.security.crypto import hash_password - # ==================== 设置 API ==================== @@ -64,10 +63,11 @@ def api_get_settings() -> Any: def api_update_settings() -> Any: """更新设置""" # 延迟导入避免循环依赖 - from outlook_web.services import scheduler as scheduler_service - from outlook_web.services import graph as graph_service from flask import current_app + from outlook_web.services import graph as graph_service + from outlook_web.services import scheduler as scheduler_service + data = request.json updated = [] errors = [] diff --git a/outlook_web/controllers/system.py b/outlook_web/controllers/system.py index fd758ff..75aefdd 100644 --- a/outlook_web/controllers/system.py +++ b/outlook_web/controllers/system.py @@ -17,7 +17,6 @@ from outlook_web.db import ( from outlook_web.repositories import settings as settings_repo from outlook_web.security.auth import login_required - # 常量 REFRESH_LOCK_NAME = "token_refresh" diff --git a/outlook_web/middleware/__init__.py b/outlook_web/middleware/__init__.py index 4c6b334..23c10ec 100644 --- a/outlook_web/middleware/__init__.py +++ b/outlook_web/middleware/__init__.py @@ -1,11 +1,11 @@ # Middleware module -from outlook_web.middleware.trace import ( - ensure_trace_id, - attach_trace_id_and_normalize_errors, -) from outlook_web.middleware.error_handler import ( - handle_http_exception, handle_exception, + handle_http_exception, +) +from outlook_web.middleware.trace import ( + attach_trace_id_and_normalize_errors, + ensure_trace_id, ) __all__ = [ diff --git a/outlook_web/repositories/distributed_locks.py b/outlook_web/repositories/distributed_locks.py index 5a1bd97..9ec2811 100644 --- a/outlook_web/repositories/distributed_locks.py +++ b/outlook_web/repositories/distributed_locks.py @@ -1,10 +1,9 @@ from __future__ import annotations +import sqlite3 import time from typing import Any, Dict, Optional -import sqlite3 - def acquire_distributed_lock( conn: sqlite3.Connection, diff --git a/outlook_web/security/csrf.py b/outlook_web/security/csrf.py index 78cc8fa..b2fd785 100644 --- a/outlook_web/security/csrf.py +++ b/outlook_web/security/csrf.py @@ -2,9 +2,9 @@ from __future__ import annotations from typing import Callable, Optional, Tuple - try: - from flask_wtf.csrf import CSRFProtect, generate_csrf as _generate_csrf # type: ignore + from flask_wtf.csrf import CSRFProtect + from flask_wtf.csrf import generate_csrf as _generate_csrf # type: ignore CSRF_AVAILABLE = True except ImportError: # pragma: no cover diff --git a/outlook_web/services/imap.py b/outlook_web/services/imap.py index 2100726..6e89c57 100644 --- a/outlook_web/services/imap.py +++ b/outlook_web/services/imap.py @@ -8,8 +8,8 @@ from typing import Any, Dict, List, Optional import requests from outlook_web.errors import build_error_payload -from outlook_web.services.http import get_response_details from outlook_web.services.graph import get_access_token_graph +from outlook_web.services.http import get_response_details # Token 端点 TOKEN_URL_IMAP = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token" diff --git a/outlook_web/services/refresh.py b/outlook_web/services/refresh.py index 3c5e736..0f69ccc 100644 --- a/outlook_web/services/refresh.py +++ b/outlook_web/services/refresh.py @@ -8,13 +8,12 @@ from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple from outlook_web.db import create_sqlite_connection from outlook_web.errors import build_error_payload, generate_trace_id -from outlook_web.security.crypto import decrypt_data from outlook_web.repositories.distributed_locks import ( acquire_distributed_lock, release_distributed_lock, ) from outlook_web.repositories.refresh_runs import create_refresh_run, finish_refresh_run - +from outlook_web.security.crypto import decrypt_data REFRESH_LOCK_TTL_SECONDS = 60 * 60 * 2 # 2 小时,避免异常中断导致长时间卡死 diff --git a/outlook_web/services/scheduler.py b/outlook_web/services/scheduler.py index ea14d03..c350c50 100644 --- a/outlook_web/services/scheduler.py +++ b/outlook_web/services/scheduler.py @@ -161,9 +161,10 @@ def init_scheduler(app, test_refresh_token): return _scheduler_instance try: - from apscheduler.schedulers.background import BackgroundScheduler import atexit + from apscheduler.schedulers.background import BackgroundScheduler + scheduler = BackgroundScheduler() configure_scheduler_jobs(scheduler, app, test_refresh_token) atexit.register(lambda: scheduler.shutdown()) diff --git a/outlook_web/services/verification_extractor.py b/outlook_web/services/verification_extractor.py index 9a4ad21..0f7fd74 100644 --- a/outlook_web/services/verification_extractor.py +++ b/outlook_web/services/verification_extractor.py @@ -13,8 +13,7 @@ from __future__ import annotations import html import re from html.parser import HTMLParser -from typing import List, Optional, Dict, Any - +from typing import Any, Dict, List, Optional # 验证码关键词列表(支持中英文) VERIFICATION_KEYWORDS = [ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2bdc945 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 127 + +[tool.isort] +profile = "black" +line_length = 127 diff --git a/start.py b/start.py index 8b294b2..2c9a09a 100644 --- a/start.py +++ b/start.py @@ -5,6 +5,7 @@ """ import os import sys + from dotenv import load_dotenv # 加载 .env 文件中的环境变量 diff --git a/tests/test_api_extract_verification.py b/tests/test_api_extract_verification.py index 7405238..64bcbca 100644 --- a/tests/test_api_extract_verification.py +++ b/tests/test_api_extract_verification.py @@ -8,10 +8,10 @@ - 设置 API 测试(轮询配置) """ -import unittest -import sys import os +import sys import time +import unittest # 添加项目根目录到 Python 路径 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) diff --git a/tests/test_core_features.py b/tests/test_core_features.py index 5f704f9..f88deae 100644 --- a/tests/test_core_features.py +++ b/tests/test_core_features.py @@ -3,7 +3,7 @@ import unittest import uuid from unittest.mock import patch -from tests._import_app import import_web_app_module, clear_login_attempts +from tests._import_app import clear_login_attempts, import_web_app_module class CoreFeatureTests(unittest.TestCase): diff --git a/tests/test_error_and_trace.py b/tests/test_error_and_trace.py index e644532..44f267d 100644 --- a/tests/test_error_and_trace.py +++ b/tests/test_error_and_trace.py @@ -3,7 +3,7 @@ import unittest import uuid from unittest.mock import patch -from tests._import_app import import_web_app_module, clear_login_attempts +from tests._import_app import clear_login_attempts, import_web_app_module class ErrorAndTraceTests(unittest.TestCase): diff --git a/tests/test_gptmail_error_handling.py b/tests/test_gptmail_error_handling.py index 79cc28d..e27dd11 100644 --- a/tests/test_gptmail_error_handling.py +++ b/tests/test_gptmail_error_handling.py @@ -1,5 +1,6 @@ import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + import requests from outlook_web.services import gptmail diff --git a/tests/test_masking_audit_and_import.py b/tests/test_masking_audit_and_import.py index e5152ed..e3f1951 100644 --- a/tests/test_masking_audit_and_import.py +++ b/tests/test_masking_audit_and_import.py @@ -2,7 +2,7 @@ import unittest import uuid from unittest.mock import patch -from tests._import_app import import_web_app_module, clear_login_attempts +from tests._import_app import clear_login_attempts, import_web_app_module class MaskingAuditAndImportTests(unittest.TestCase): diff --git a/tests/test_performance.py b/tests/test_performance.py index 8e7a44c..3933813 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -7,10 +7,10 @@ - 轮询性能测试(不影响页面交互) """ -import unittest -import sys import os +import sys import time +import unittest # 添加项目根目录到 Python 路径 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) diff --git a/tests/test_smoke_contract.py b/tests/test_smoke_contract.py index d11f925..d3f54b5 100644 --- a/tests/test_smoke_contract.py +++ b/tests/test_smoke_contract.py @@ -1,6 +1,6 @@ import unittest -from tests._import_app import import_web_app_module, clear_login_attempts +from tests._import_app import clear_login_attempts, import_web_app_module class SmokeContractTests(unittest.TestCase): diff --git a/tests/test_ui_redesign_bugs.py b/tests/test_ui_redesign_bugs.py index 0db4bef..981ad52 100644 --- a/tests/test_ui_redesign_bugs.py +++ b/tests/test_ui_redesign_bugs.py @@ -25,7 +25,7 @@ class TestUIRedesignBugFixes(unittest.TestCase): cls.app = cls.module.app def _login(self, client): - resp = client.post('/login', json={'password': 'testpass123'}) + resp = client.post("/login", json={"password": "testpass123"}) self.assertEqual(resp.status_code, 200) def _get_client(self): @@ -39,8 +39,8 @@ class TestUIRedesignBugFixes(unittest.TestCase): 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') + resp = client.get("/") + html = resp.data.decode("utf-8") self.assertIn('id="currentAccountBar"', html) self.assertIn('id="currentAccountEmail"', html) self.assertIn('id="currentAccount"', html) @@ -48,18 +48,19 @@ 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') - self.assertIn('currentAccountBar', js, - "selectAccount() 应操作 #currentAccountBar 元素") + resp = client.get("/static/js/features/accounts.js") + js = resp.data.decode("utf-8") + self.assertIn( + "currentAccountBar", js, "selectAccount() 应操作 #currentAccountBar 元素" + ) # ==================== BUG-003: 设置页面 ==================== def test_bug003_settings_page_has_content(self): """BUG-003: 设置页面应有实质性内容,而非仅占位文字""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') + resp = client.get("/") + html = resp.data.decode("utf-8") settings_section = re.search( r'id="page-settings".*?(?=id="page-|$)', html, re.DOTALL ) @@ -68,8 +69,8 @@ class TestUIRedesignBugFixes(unittest.TestCase): 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') + resp = client.get("/static/js/main.js") + js = resp.data.decode("utf-8") self.assertIn("settings", js) # ==================== BUG-004: 弹窗居中 ==================== @@ -77,122 +78,140 @@ 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') - self.assertIn('align-items: center', css) - self.assertIn('justify-content: center', css) + resp = client.get("/static/css/main.css") + css = resp.data.decode("utf-8") + 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') - self.assertIn('max-width: 560px', css) + resp = client.get("/static/css/main.css") + css = resp.data.decode("utf-8") + self.assertIn("max-width: 560px", css) def test_bug004_all_modals_use_modal_class(self): - """BUG-004: modals.html 中所有弹窗应使用 class='modal' """ + """BUG-004: modals.html 中所有弹窗应使用 class='modal'""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') + resp = client.get("/") + html = resp.data.decode("utf-8") modal_ids = re.findall(r'id="(\w+Modal)"', html) for modal_id in modal_ids: - if modal_id == 'fullscreenEmailModal': + if modal_id == "fullscreenEmailModal": continue pattern = rf']*id="{modal_id}"[^>]*>' match = re.search(pattern, html) self.assertIsNotNone(match, f"找不到弹窗 {modal_id}") tag = match.group(0) - self.assertIn('class="modal"', tag, - f"弹窗 {modal_id} 应使用 class='modal',实际: {tag[:80]}") + self.assertIn( + 'class="modal"', + tag, + f"弹窗 {modal_id} 应使用 class='modal',实际: {tag[:80]}", + ) # ==================== BUG-005: 验证码提取 ==================== 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") + resp = client.get("/api/emails/nonexistent@test.com/extract-verification") + self.assertIn( + resp.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') - self.assertIn('function copyVerificationInfo', js) + resp = client.get("/static/js/features/groups.js") + js = resp.data.decode("utf-8") + self.assertIn("function copyVerificationInfo", js) # ==================== BUG-006: 卡片颜色 ==================== 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') - self.assertIn('renderTempEmailList', js) + resp = client.get("/static/js/features/temp_emails.js") + js = resp.data.decode("utf-8") + self.assertIn("renderTempEmailList", js) # ==================== BUG-010: 仪表盘 ==================== def test_bug010_dashboard_elements_exist(self): """BUG-010: 仪表盘统计元素存在""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') - for el_id in ['statTotalAccounts', 'statValidTokens', - 'statExpiredTokens', 'statTempEmails']: - self.assertIn(f'id="{el_id}"', html, - f"仪表盘应包含 #{el_id} 元素") + resp = client.get("/") + html = resp.data.decode("utf-8") + for el_id in [ + "statTotalAccounts", + "statValidTokens", + "statExpiredTokens", + "statTempEmails", + ]: + self.assertIn(f'id="{el_id}"', html, f"仪表盘应包含 #{el_id} 元素") def test_bug010_dashboard_group_list_exists(self): """BUG-010: 仪表盘分组概览列表存在""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') + resp = client.get("/") + html = resp.data.decode("utf-8") 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') - self.assertIn('function loadDashboard', js) - self.assertIn('/api/groups', js) + resp = client.get("/static/js/main.js") + js = resp.data.decode("utf-8") + 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') + resp = client.get("/api/groups") data = resp.get_json() - self.assertTrue(data.get('success'), "分组 API 应返回 success=true") - self.assertIn('groups', data, "分组 API 应包含 groups 字段") + self.assertTrue(data.get("success"), "分组 API 应返回 success=true") + self.assertIn("groups", data, "分组 API 应包含 groups 字段") # ==================== 结构完整性 ==================== def test_all_pages_exist_in_html(self): """所有页面容器存在于 index.html""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') - pages = ['page-dashboard', 'page-mailbox', 'page-temp-emails', - 'page-refresh-log', 'page-settings', 'page-audit'] + resp = client.get("/") + html = resp.data.decode("utf-8") + pages = [ + "page-dashboard", + "page-mailbox", + "page-temp-emails", + "page-refresh-log", + "page-settings", + "page-audit", + ] for page_id in pages: self.assertIn(f'id="{page_id}"', html, f"缺少页面 #{page_id}") def test_sidebar_navigation_items(self): """侧边栏导航项完整""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') - nav_pages = ['dashboard', 'mailbox', 'temp-emails', - 'refresh-log', 'settings', 'audit'] + resp = client.get("/") + html = resp.data.decode("utf-8") + nav_pages = [ + "dashboard", + "mailbox", + "temp-emails", + "refresh-log", + "settings", + "audit", + ] for page in nav_pages: - self.assertIn(f'data-page="{page}"', html, - f"侧边栏缺少 {page} 导航项") + self.assertIn(f'data-page="{page}"', html, f"侧边栏缺少 {page} 导航项") def test_mailbox_three_column_structure(self): """邮箱管理三栏布局结构完整""" client = self._get_client() - resp = client.get('/') - html = resp.data.decode('utf-8') + resp = client.get("/") + html = resp.data.decode("utf-8") self.assertIn('class="mailbox-layout"', html) self.assertIn('class="groups-column"', html) self.assertIn('class="accounts-column"', html) @@ -202,29 +221,27 @@ class TestUIRedesignBugFixes(unittest.TestCase): """所有 JS 文件可正常加载""" client = self._get_client() js_files = [ - '/static/js/main.js', - '/static/js/features/groups.js', - '/static/js/features/accounts.js', - '/static/js/features/emails.js', - '/static/js/features/temp_emails.js', + "/static/js/main.js", + "/static/js/features/groups.js", + "/static/js/features/accounts.js", + "/static/js/features/emails.js", + "/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} 内容不应为空") + self.assertEqual(resp.status_code, 200, f"{js_path} 应返回 200") + self.assertGreater(len(resp.data), 100, f"{js_path} 内容不应为空") def test_css_loads_successfully(self): """CSS 文件可正常加载""" client = self._get_client() - resp = client.get('/static/css/main.css') + resp = client.get("/static/css/main.css") self.assertEqual(resp.status_code, 200) - css = resp.data.decode('utf-8') - self.assertIn('--clr-primary', css) - self.assertIn('--bg-sidebar', css) - self.assertIn('--bg-hover', css) + css = resp.data.decode("utf-8") + self.assertIn("--clr-primary", css) + self.assertIn("--bg-sidebar", css) + self.assertIn("--bg-hover", css) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_verification_extractor.py b/tests/test_verification_extractor.py index 4969c0a..8c67831 100644 --- a/tests/test_verification_extractor.py +++ b/tests/test_verification_extractor.py @@ -8,20 +8,20 @@ - 边界情况处理 """ -import unittest -import sys import os +import sys +import unittest # 添加项目根目录到 Python 路径 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from outlook_web.services.verification_extractor import ( - smart_extract_verification_code, - fallback_extract_verification_code, - extract_links, extract_email_text, + extract_links, extract_verification_info, extract_verification_info_from_text, + fallback_extract_verification_code, + smart_extract_verification_code, ) diff --git a/web_outlook_app.py b/web_outlook_app.py index d328347..73f3eaa 100644 --- a/web_outlook_app.py +++ b/web_outlook_app.py @@ -17,19 +17,19 @@ Outlook 邮件 Web 应用(兼容入口) import os from outlook_web.app import create_app -from outlook_web.services import scheduler as scheduler_service -from outlook_web.services import graph as graph_service from outlook_web.db import create_sqlite_connection - -# 兼容导入:从各模块导出常用函数 -from outlook_web.security.auth import MAX_LOGIN_ATTEMPTS -from outlook_web.errors import sanitize_error_details, build_error_payload -from outlook_web.security.crypto import decrypt_data, encrypt_data +from outlook_web.errors import build_error_payload, sanitize_error_details from outlook_web.repositories.distributed_locks import ( acquire_distributed_lock, release_distributed_lock, ) +# 兼容导入:从各模块导出常用函数 +from outlook_web.security.auth import MAX_LOGIN_ATTEMPTS +from outlook_web.security.crypto import decrypt_data, encrypt_data +from outlook_web.services import graph as graph_service +from outlook_web.services import scheduler as scheduler_service + # 在脚本运行场景(__main__)中,调度器由 main block 统一控制, # 避免 debug reloader 父进程误启后台线程。 app = create_app(autostart_scheduler=None if __name__ != "__main__" else False)