Files
daily_stock_analysis/tests/test_auth_status_setup_state.py
LouisHong f3a8993283 #602 [PR 5 + PR 6] 全局导航与应用外壳 & 系统管理与登录模块 (#714)
* 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>
2026-03-17 21:35:17 +08:00

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()