mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* refactor(web): rebuild shared ui primitives and tokens * feat(web): add shell, theming, and page layout integration * feat(auth): expose setup-state auth contract * feat(web): rebuild login, settings, and auth flows * test(web): add smoke coverage and ui regressions * feat(web): add page titles to all pages Add document.title to all pages for better browser tab identification: - HomePage: '每日选股分析 - DSA' - ChatPage: '策略问股 - DSA' - SettingsPage: '系统设置 - DSA' - BacktestPage: '策略回测 - DSA' - PortfolioPage: '持仓分析 - DSA' - LoginPage: '登录 - DSA' - NotFoundPage: '页面未找到 - DSA' All pages use useEffect to set the title on mount. * feat(web): enhance UI with ongoing adjustments and improvements; update README for user guidance feat(tests): add unit tests for useSystemConfig hook to ensure stability and functionality chore(changelog): document major updates including UI refresh, auth workflow overhaul, and test coverage enhancements * fix: encode non-ascii email sender names (#712) * fix: encode non-ascii email sender names * fix: clarify _close_server silent-exception intent and add inline-image sender name test fixes #708 * docs: add EN doc index, contributing guide, bot guide; bilingual issue/PR templates (#713) * docs: add EN doc index, contributing guide, bot guide; bilingual issue/PR templates - Add docs/INDEX_EN.md: full English docs index with China-market glossary - Add docs/CONTRIBUTING_EN.md: English contributing guide (setup, CI, commit conventions) - Add docs/bot-command_EN.md: English bot integration guide (commands, webhooks, config) - Bilingualize .github/ISSUE_TEMPLATE/bug_report.md and feature_request.md - Update .github/ISSUE_TEMPLATE/config.yml with English Docs Index link - Bilingualize .github/PULL_REQUEST_TEMPLATE.md checklist and field labels - Add CONTRIBUTING_EN and INDEX_EN links to docs/README_EN.md nav bar Refs #711 * docs: fix review feedback on bot-command_EN and CONTRIBUTING_EN - Correct bot/platforms/ directory tree to match actual files (feishu_stream.py+discord.py present; feishu.py/wecom.py/telegram.py absent) - Add missing commands: /ask, /chat, /batch to commands table - Fix BotCommand.execute() signature: sync def, not async - Clarify webhook routes as planned/not-yet-registered in FastAPI; point to bot/handler.py as the actual implementation location - Fix backend-gate description in CI table to include ./test.sh code and ./test.sh yfinance steps from ci_gate.sh * docs: fix format_response signature and webhook route status in bot-command_EN - format_response: correct signature to (response, message) -> WebhookResponse to match bot/platforms/base.py abstract method - Webhook route table: clarify that only dingtalk is in ALL_PLATFORMS (webhook mode ready); feishu is stream-only; wecom/telegram not yet implemented - Add concrete example for mounting dingtalk webhook in FastAPI * docs: fix remaining review feedback in EN docs - bot-command_EN: stop claiming bot env keys are in .env.example - bot-command_EN: mount webhook routes in api/app.py instead of api/v1/router.py - CONTRIBUTING_EN: keep PR CI table limited to actual pull-request checks - CONTRIBUTING_EN: clarify network-smoke is schedule/workflow_dispatch only * docs: clarify EN issue links and bot env guidance * feat(ui): refine settings actions and import conflict recovery * feat(auth): implement session invalidation on logout and handle errors --------- Co-authored-by: mumu <42829555+ZhuLinsen@users.noreply.github.com>
139 lines
5.4 KiB
Python
139 lines
5.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Unit tests for Auth setupState contract in /auth/status and /auth/settings."""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from starlette.requests import Request
|
|
|
|
import src.auth as auth
|
|
from api.v1.endpoints.auth import AuthSettingsRequest, auth_status, auth_update_settings
|
|
|
|
|
|
def _reset_auth_globals() -> None:
|
|
"""Reset auth module globals for test isolation."""
|
|
auth._auth_enabled = None
|
|
auth._session_secret = None
|
|
auth._password_hash_salt = None
|
|
auth._password_hash_stored = None
|
|
auth._rate_limit = {}
|
|
|
|
|
|
def _make_request(*, cookies: dict[str, str] | None = None) -> Request:
|
|
"""Create a minimal Starlette request for endpoint unit tests."""
|
|
headers: list[tuple[bytes, bytes]] = []
|
|
if cookies:
|
|
cookie_header = "; ".join(f"{key}={value}" for key, value in cookies.items())
|
|
headers.append((b"cookie", cookie_header.encode("utf-8")))
|
|
|
|
scope = {
|
|
"type": "http",
|
|
"asgi": {"version": "3.0"},
|
|
"http_version": "1.1",
|
|
"method": "GET",
|
|
"scheme": "http",
|
|
"path": "/api/v1/auth/status",
|
|
"raw_path": b"/api/v1/auth/status",
|
|
"query_string": b"",
|
|
"headers": headers,
|
|
"client": ("127.0.0.1", 12345),
|
|
"server": ("testserver", 80),
|
|
}
|
|
return Request(scope)
|
|
|
|
|
|
class AuthStatusSetupStateTestCase(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
_reset_auth_globals()
|
|
self.temp_dir = tempfile.TemporaryDirectory()
|
|
self.data_dir = Path(self.temp_dir.name)
|
|
|
|
self._data_dir_patcher = patch.object(auth, "_get_data_dir", return_value=self.data_dir)
|
|
self._data_dir_patcher.start()
|
|
|
|
self.env_path = self.data_dir / ".env"
|
|
self.env_path.write_text("ADMIN_AUTH_ENABLED=false\n", encoding="utf-8")
|
|
self._env_patcher = patch.dict(os.environ, {"ENV_FILE": str(self.env_path)})
|
|
self._env_patcher.start()
|
|
|
|
def tearDown(self) -> None:
|
|
self._env_patcher.stop()
|
|
self._data_dir_patcher.stop()
|
|
_reset_auth_globals()
|
|
self.temp_dir.cleanup()
|
|
|
|
def test_status_no_password(self) -> None:
|
|
"""Scenario: Auth disabled and no password set."""
|
|
request = _make_request()
|
|
with patch("api.v1.endpoints.auth.is_auth_enabled", return_value=False):
|
|
with patch("src.auth.is_auth_enabled", return_value=False):
|
|
data = asyncio.run(auth_status(request))
|
|
self.assertEqual(data["setupState"], "no_password")
|
|
self.assertFalse(data["authEnabled"])
|
|
|
|
def test_status_password_retained(self) -> None:
|
|
"""Scenario: Auth disabled but password exists on disk."""
|
|
auth.set_initial_password("password123")
|
|
request = _make_request()
|
|
|
|
with patch("api.v1.endpoints.auth.is_auth_enabled", return_value=False):
|
|
with patch("src.auth.is_auth_enabled", return_value=False):
|
|
data = asyncio.run(auth_status(request))
|
|
self.assertEqual(data["setupState"], "password_retained")
|
|
self.assertFalse(data["authEnabled"])
|
|
self.assertFalse(data["passwordSet"])
|
|
|
|
def test_status_enabled(self) -> None:
|
|
"""Scenario: Auth enabled."""
|
|
auth.set_initial_password("password123")
|
|
request = _make_request()
|
|
|
|
with patch("api.v1.endpoints.auth.is_auth_enabled", return_value=True):
|
|
with patch("src.auth.is_auth_enabled", return_value=True):
|
|
data = asyncio.run(auth_status(request))
|
|
self.assertEqual(data["setupState"], "enabled")
|
|
self.assertTrue(data["authEnabled"])
|
|
self.assertTrue(data["passwordSet"])
|
|
|
|
def test_settings_update_returns_setup_state(self) -> None:
|
|
"""Verify that /auth/settings also returns setupState in response."""
|
|
request = _make_request()
|
|
body = AuthSettingsRequest(
|
|
authEnabled=True,
|
|
password="newpassword123",
|
|
passwordConfirm="newpassword123",
|
|
)
|
|
|
|
with patch("api.v1.endpoints.auth.is_auth_enabled") as mock_endpoint_enabled:
|
|
with patch("src.auth.is_auth_enabled") as mock_src_enabled:
|
|
mock_src_enabled.return_value = False
|
|
mock_endpoint_enabled.return_value = False
|
|
|
|
with patch("api.v1.endpoints.auth._apply_auth_enabled", return_value=True):
|
|
with patch("api.v1.endpoints.auth.rotate_session_secret", return_value=True):
|
|
with patch("api.v1.endpoints.auth.create_session", return_value="mock.session.sig"):
|
|
with patch("api.v1.endpoints.auth._get_auth_status_dict") as mock_status_dict:
|
|
mock_status_dict.return_value = {
|
|
"authEnabled": True,
|
|
"loggedIn": True,
|
|
"passwordSet": True,
|
|
"passwordChangeable": True,
|
|
"setupState": "enabled",
|
|
}
|
|
|
|
response = asyncio.run(auth_update_settings(request, body))
|
|
|
|
self.assertEqual(response.status_code, 200)
|
|
data = json.loads(response.body)
|
|
self.assertEqual(data["setupState"], "enabled")
|
|
self.assertTrue(data["authEnabled"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|