from __future__ import annotations import sqlite3 import tempfile import unittest from pathlib import Path from types import SimpleNamespace from database import ReviewDatabase from server import DashboardService, RequestHandler FORMULA = {"all": [{"field": "change", "operator": ">", "value": 0}]} class AccountDataBoundaryTests(unittest.TestCase): def setUp(self) -> None: self.temp = tempfile.TemporaryDirectory() self.database = ReviewDatabase(Path(self.temp.name) / "review.db") self.first = self.database.create_user("first_user", "salt", "hash") self.second = self.database.create_user("second_user", "salt", "hash") def tearDown(self) -> None: self.temp.cleanup() def test_custom_strategies_are_private_and_builtins_are_shared(self): builtin_id = self.database.save_screener_strategy( None, "共享策略", "", ["repair"], FORMULA, builtin=True ) private_id = self.database.save_screener_strategy( self.first["id"], "甲的策略", "", ["repair"], FORMULA ) first_names = {item["name"] for item in self.database.list_screener_strategies(self.first["id"])} second_names = {item["name"] for item in self.database.list_screener_strategies(self.second["id"])} self.assertEqual(first_names, {"共享策略", "甲的策略"}) self.assertEqual(second_names, {"共享策略"}) with self.assertRaises(ValueError): self.database.delete_screener_strategy(self.second["id"], private_id) with self.assertRaises(ValueError): self.database.delete_screener_strategy(self.first["id"], builtin_id) self.assertTrue(self.database.delete_screener_strategy(self.first["id"], private_id)) def test_screener_runs_are_private(self): self.database.save_screener_run( self.first["id"], "20260721", "repair", "甲的策略", FORMULA, {"candidates": [{"code": "002141"}], "meta": {}}, ) self.assertEqual( self.database.latest_screener_run(self.first["id"], "20260722")["candidates"][0]["code"], "002141", ) self.assertIsNone(self.database.latest_screener_run(self.second["id"], "20260722")) def test_latest_screener_runs_are_isolated_by_mode_and_user(self): expected = { "smart": "600001", "curated": "600002", "quant": "600003", } for mode, code in expected.items(): self.database.save_screener_run( self.first["id"], "20260721", "repair", f"{mode}-strategy", FORMULA, {"candidates": [{"code": code}], "meta": {}}, mode, ) results = self.database.latest_screener_runs(self.first["id"], "20260722") self.assertEqual(set(results), set(expected)) for mode, code in expected.items(): self.assertEqual(results[mode]["meta"]["mode"], mode) self.assertEqual(results[mode]["candidates"][0]["code"], code) self.assertEqual( self.database.latest_screener_run( self.first["id"], "20260722", mode )["candidates"][0]["code"], code, ) self.assertEqual( self.database.latest_screener_runs(self.second["id"], "20260722"), {} ) def test_latest_screener_context_runs_keep_each_stage_and_strategy(self): runs = [ ("smart", "repair", "Repair", "600001"), ("smart", "repair", "Repair", "600002"), ("smart", "retreat", "Retreat", "600003"), ("curated", "repair", "Dividend", "600004"), ("curated", "repair", "Momentum", "600005"), ("quant", "repair", "Custom quant", "600006"), ("quant", "repair", "Custom quant", "600007"), ] for mode, regime, strategy, code in runs: self.database.save_screener_run( self.first["id"], "20260722", regime, strategy, FORMULA, {"candidates": [{"code": code}], "meta": {}}, mode, ) results = self.database.latest_screener_context_runs( self.first["id"], "20260722" ) by_context = { ( item["meta"]["mode"], item["meta"]["regime"] if item["meta"]["mode"] == "smart" else "", item["meta"]["strategy_name"] if item["meta"]["mode"] != "quant" else "", ): item["candidates"][0]["code"] for item in results } self.assertEqual(by_context, { ("smart", "repair", "Repair"): "600002", ("smart", "retreat", "Retreat"): "600003", ("curated", "", "Dividend"): "600004", ("curated", "", "Momentum"): "600005", ("quant", "", ""): "600007", }) self.assertEqual( self.database.latest_screener_context_runs( self.second["id"], "20260722" ), [], ) def test_mentor_messages_are_scoped_by_user_mentor_and_date(self): self.database.save_mentor_exchange( self.first["id"], "mentor-a", "20260721", "怎么看?", "先看承接。", "20260721" ) self.assertEqual( [item["role"] for item in self.database.list_mentor_messages( self.first["id"], "mentor-a", "20260721" )], ["user", "assistant"], ) self.assertEqual( self.database.list_mentor_messages(self.second["id"], "mentor-a", "20260721"), [] ) self.assertEqual( self.database.list_mentor_messages(self.first["id"], "mentor-b", "20260721"), [] ) self.assertEqual( self.database.list_mentor_messages(self.first["id"], "mentor-a", "20260722"), [] ) self.assertEqual( self.database.delete_mentor_messages(self.second["id"], "mentor-a", "20260721"), 0 ) self.assertEqual( self.database.delete_mentor_messages(self.first["id"], "mentor-a", "20260721"), 2 ) def test_mentor_preferences_are_scoped_by_user(self): self.database.save_mentor_preferences( self.first["id"], ["mentor-b", "mentor-a"], {"mentor-b"} ) self.database.save_mentor_preferences( self.second["id"], ["mentor-a", "mentor-b"], set() ) first = self.database.list_mentor_preferences(self.first["id"]) second = self.database.list_mentor_preferences(self.second["id"]) self.assertEqual([item["mentor_id"] for item in first], ["mentor-b", "mentor-a"]) self.assertTrue(first[0]["pinned"]) self.assertEqual([item["mentor_id"] for item in second], ["mentor-a", "mentor-b"]) self.assertFalse(any(item["pinned"] for item in second)) def test_latest_data_snapshot_skips_demo_and_future_records(self): self.database.save_data_snapshot( "stock_detail", "002141:20260718", "tushare", {"marker": "real"} ) self.database.save_data_snapshot( "stock_detail", "002141:20260719", "demo", {"marker": "demo"} ) self.database.save_data_snapshot( "stock_detail", "002141:20260723", "tushare", {"marker": "future"} ) payload = self.database.get_latest_data_snapshot( "stock_detail", "002141:", "002141:20260722", exclude_source="demo" ) self.assertEqual(payload["marker"], "real") def test_stock_detail_never_falls_back_to_demo_data(self): self.database.save_data_snapshot( "stock_detail", "002141:20260721", "demo", {"meta": {"source": "demo"}, "stock": {"code": "002141"}, "prices": [{}]}, ) service = DashboardService.__new__(DashboardService) service.database = self.database service._system_credentials = {"tushare_token": ""} service._request_context = SimpleNamespace(user_id=self.first["id"]) with self.assertRaisesRegex(ValueError, "暂无 002141 的真实行情数据"): service.get_stock_detail("002141", "2026-07-22") class LegacyStrategyMigrationTests(unittest.TestCase): def test_legacy_custom_strategy_and_run_move_to_first_admin(self): with tempfile.TemporaryDirectory() as directory: database_path = Path(directory) / "legacy.db" connection = sqlite3.connect(database_path) try: connection.executescript( """ CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_salt TEXT NOT NULL, password_hash TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); INSERT INTO users VALUES (1, 'legacy_admin', 'salt', 'hash', '2026-01-01', '2026-01-01'); CREATE TABLE screener_strategies ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT NOT NULL DEFAULT '', regimes TEXT NOT NULL, formula TEXT NOT NULL, builtin INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); INSERT INTO screener_strategies VALUES (1, '旧策略', '', '["repair"]', '{"all":[]}', 0, '2026-01-01', '2026-01-01'), (2, '旧内置', '', '["repair"]', '{"all":[]}', 1, '2026-01-01', '2026-01-01'); CREATE TABLE screener_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, trade_date TEXT NOT NULL, regime TEXT NOT NULL, strategy_name TEXT NOT NULL, formula TEXT NOT NULL, result TEXT NOT NULL, created_at TEXT NOT NULL ); INSERT INTO screener_runs VALUES (1, '20260721', 'repair', '旧策略', '{}', '{"meta":{}}', '2026-07-21'), (2, '20260721', 'repair', '旧精选策略', '{"meta":{"library":"curated"}}', '{"meta":{}}', '2026-07-21'), (3, '20260721', 'repair', '自定义量化公式', '{"meta":{"library":"custom","category":"量化公式"}}', '{"meta":{}}', '2026-07-21'); """ ) connection.commit() finally: connection.close() migrated = ReviewDatabase(database_path) strategies = migrated.list_screener_strategies(1) owners = {item["name"]: item["user_id"] for item in strategies} self.assertEqual(owners["旧策略"], 1) self.assertIsNone(owners["旧内置"]) self.assertIsNotNone(migrated.latest_screener_run(1, "20260722")) self.assertEqual( set(migrated.latest_screener_runs(1, "20260722")), {"smart", "curated", "quant"}, ) class PublicKnowledgePermissionTests(unittest.TestCase): @staticmethod def handler(path: str, method_name: str): handler = RequestHandler.__new__(RequestHandler) handler.path = path handler.require_auth = lambda: True handler.require_csrf = lambda: True handler.require_admin = lambda: False handler.require_member = lambda: True setattr(handler, method_name, lambda: (_ for _ in ()).throw(AssertionError("mutation ran"))) return handler def test_regular_user_cannot_change_reason_or_seat_alias(self): RequestHandler.do_POST(self.handler("/api/reasons", "save_reason")) RequestHandler.do_POST(self.handler("/api/seat-aliases", "save_seat_alias")) def test_regular_user_cannot_change_sector_phase(self): RequestHandler.do_POST( self.handler("/api/heaven/sector-phases", "save_sector_phase_override") ) RequestHandler.do_DELETE( self.handler("/api/heaven/sector-phases/%E6%B2%B9%E6%B0%94", "send_json") ) def test_static_shell_bypasses_the_api_access_registry(self): handler = RequestHandler.__new__(RequestHandler) handler.path = "/" handler.require_auth = lambda: (_ for _ in ()).throw( AssertionError("static request required authentication") ) handler.require_access = lambda *_: (_ for _ in ()).throw( AssertionError("static request entered the API registry") ) served: list[str] = [] handler.serve_static = served.append RequestHandler.do_GET(handler) self.assertEqual(served, ["/"]) if __name__ == "__main__": unittest.main()