migration: preserve startup accounts and system slice

This commit is contained in:
leefer
2026-07-31 00:42:06 +08:00
parent 4083dceba3
commit 4002f096f4
37 changed files with 6821 additions and 6327 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ import sqlite3
from pathlib import Path
from database import ReviewDatabase
from security import hash_password, verify_password
from backend.features.accounts.security import hash_password, verify_password
class AccountAccessTests(unittest.TestCase):
+3 -1
View File
@@ -47,7 +47,9 @@ class DataGatewayTests(unittest.TestCase):
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
from pathlib import Path
source = (Path(__file__).resolve().parents[1] / "server.py").read_text(encoding="utf-8")
source = (
Path(__file__).resolve().parents[1] / "backend" / "application.py"
).read_text(encoding="utf-8")
self.assertEqual(source.count("TushareClient(self.token)"), 1)
self.assertIn("return gateway.tushare()", source)
+1 -1
View File
@@ -9,7 +9,7 @@ from tushare_client import _sector_coverage_issue
def load_method(name: str):
source = Path("server.py").read_text(encoding="utf-8")
source = Path("backend/application.py").read_text(encoding="utf-8")
tree = ast.parse(source)
dashboard_service = next(
node for node in tree.body
+1 -1
View File
@@ -81,7 +81,7 @@ class MentorSkillRegistryTests(unittest.TestCase):
self.assertTrue(all(item.quality_total == 6 for item in skills))
def test_server_applies_private_guard_to_every_mentor_entry_point(self):
source = (ROOT / "server.py").read_text(encoding="utf-8")
source = (ROOT / "backend" / "application.py").read_text(encoding="utf-8")
mentor_section = source[source.index(" def mentor_setup"):source.index(" def _heaven_manual_schema")]
self.assertGreaterEqual(
mentor_section.count('include_private=self.membership()["is_admin"]'),
@@ -0,0 +1,116 @@
from __future__ import annotations
import tempfile
import threading
import unittest
from pathlib import Path
import server
from backend.application import DashboardService, RequestHandler
from backend.bootstrap.config import APP_DIR, STATIC_DIR
from backend.features.accounts.repository import AccountRepositoryMixin
from backend.features.accounts.security import SecretVault, token_hash
from backend.features.accounts.service import AccountService
from backend.http.handler import HttpTransportMixin
from database import ReviewDatabase
class AccountSliceStructureTests(unittest.TestCase):
def test_original_entrypoint_exports_canonical_runtime(self) -> None:
self.assertIs(server.DashboardService, DashboardService)
self.assertIs(server.RequestHandler, RequestHandler)
self.assertIs(server.SERVICE, RequestHandler.application_service)
def test_runtime_paths_still_point_at_app_root(self) -> None:
self.assertEqual(APP_DIR, Path(__file__).resolve().parents[1])
self.assertEqual(STATIC_DIR, APP_DIR / "static")
def test_account_persistence_and_http_transport_have_single_owners(self) -> None:
for method in (
"create_user",
"session_user",
"update_membership",
"save_user_birth_profile",
):
self.assertNotIn(method, ReviewDatabase.__dict__)
self.assertIn(method, AccountRepositoryMixin.__dict__)
for method in (
"require_auth",
"require_csrf",
"require_access",
"serve_static",
"send_json",
):
self.assertNotIn(method, RequestHandler.__dict__)
self.assertIn(method, HttpTransportMixin.__dict__)
class AccountSliceBehaviorTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.database = ReviewDatabase(Path(self.temporary.name) / "review.db")
self.vault = SecretVault(SecretVault.generate_key())
self.context: dict[str, object] = {"user_id": 0, "access": {}}
def bind_user(user_id: int) -> None:
self.context["user_id"] = user_id
self.context["access"] = self.database.user_access(user_id) or {}
self.service = AccountService(
database=self.database,
vault=self.vault,
current_user_supplier=lambda: int(self.context["user_id"]),
access_supplier=lambda: dict(self.context["access"]),
bind_user=bind_user,
personal_field_builder=lambda *args: {
"birth": "private",
"day_master": "甲木",
"current": {"trade_date": args[2]},
"notice": "test",
},
auth_lock=threading.Lock(),
)
def tearDown(self) -> None:
self.temporary.cleanup()
def test_register_login_session_and_password_contract(self) -> None:
registered = self.service.register("owner_01", "Password123")
self.assertEqual(registered["user"]["role"], "admin")
self.assertTrue(registered["user"]["membership"]["active"])
self.assertIsNotNone(
self.database.session_user(token_hash(registered["session_token"]))
)
with self.assertRaisesRegex(ValueError, "账号名或密码不正确"):
self.service.login("owner_01", "wrong-password")
self.service.change_password("Password123", "NewPassword456")
logged_in = self.service.login("owner_01", "NewPassword456")
self.assertEqual(logged_in["user"]["id"], registered["user"]["id"])
def test_membership_and_birth_profile_remain_account_scoped(self) -> None:
owner = self.service.register("owner_02", "Password123")
other = self.database.create_user("other_02", "salt", "hash")
self.service.update_membership(
{"user_id": other["id"], "status": "active", "duration": "3_months"}
)
other_access = self.database.user_access(other["id"])
self.assertEqual(other_access["membership_plan"], "3个月")
self.assertTrue(AccountService.membership_for_access(other_access)["subscribed"])
personal = self.service.save_birth_profile(
{
"birth_datetime": "1990-01-01 08:30",
"gender": "male",
"trade_date": "2026-07-30",
}
)
self.assertNotIn("birth", personal)
self.assertEqual(personal["day_master"], "甲木")
self.assertTrue(self.database.get_user_birth_profile(owner["user"]["id"]))
self.assertEqual(self.database.get_user_birth_profile(other["id"]), "")
if __name__ == "__main__":
unittest.main()
+6 -6
View File
@@ -90,8 +90,8 @@ class StockDetailRealtimeTests(unittest.TestCase):
"moneyflow": {},
}
with patch("server.datetime", FixedMarketDatetime), patch(
"server.TushareClient", RealtimeClientStub
with patch("backend.application.datetime", FixedMarketDatetime), patch(
"backend.application.TushareClient", RealtimeClientStub
):
result = self.service._prepare_stock_detail(cached, "002141", today)
@@ -112,8 +112,8 @@ class StockDetailRealtimeTests(unittest.TestCase):
"stock": {"code": "002141", "price": 10, "change": 1.2},
"prices": [{"trade_date": historical, "close": 10, "change": 1.2}],
}
with patch("server.datetime", FixedMarketDatetime), patch(
"server.TushareClient", RealtimeClientStub
with patch("backend.application.datetime", FixedMarketDatetime), patch(
"backend.application.TushareClient", RealtimeClientStub
):
result = self.service._prepare_stock_detail(payload, "002141", historical)
@@ -151,8 +151,8 @@ class StockDetailRealtimeTests(unittest.TestCase):
},
],
}
with patch("server.datetime", FixedPreopenDatetime), patch(
"server.TushareClient", RealtimeClientStub
with patch("backend.application.datetime", FixedPreopenDatetime), patch(
"backend.application.TushareClient", RealtimeClientStub
):
result = self.service._prepare_stock_detail(payload, "002141", today)