from __future__ import annotations import threading import unittest from datetime import datetime, timedelta, timezone from pathlib import Path from tempfile import TemporaryDirectory from backend.features.accounts.security import SecretVault, token_hash from backend.features.accounts.service import AccountService from database import ReviewDatabase class AccountSwitchGrantTests(unittest.TestCase): def setUp(self) -> None: self.temp = TemporaryDirectory() self.database = ReviewDatabase(Path(self.temp.name) / "review.db") self.bound_user_id = 0 self.service = AccountService( database=self.database, vault=SecretVault(SecretVault.generate_key()), current_user_supplier=lambda: self.bound_user_id, access_supplier=lambda: self.database.user_access(self.bound_user_id) or {}, bind_user=self._bind, personal_field_builder=lambda *args, **kwargs: {}, auth_lock=threading.Lock(), ) self.device_a = token_hash("device-a-token") self.device_b = token_hash("device-b-token") def tearDown(self) -> None: self.temp.cleanup() def _bind(self, user_id: int) -> None: self.bound_user_id = int(user_id) def _register(self, username: str, device_hash: str = "") -> dict: return self.service.register(username, "Password123", device_hash or self.device_a) def test_login_records_accounts_for_the_current_device_only(self) -> None: first = self._register("alpha_user") second = self._register("beta_user") self.service.login("alpha_user", "Password123", self.device_b) listed = self.service.list_device_accounts(self.device_a) names = [item["username"] for item in listed["accounts"]] self.assertEqual(names, ["beta_user", "alpha_user"]) self.assertEqual( self.service.list_device_accounts(self.device_b)["accounts"][0]["username"], "alpha_user", ) self.assertEqual(self.service.list_device_accounts("")["accounts"], []) self.assertEqual(first["user"]["username"], "alpha_user") self.assertEqual(second["user"]["username"], "beta_user") def test_switch_uses_device_grant_and_keeps_the_original_authorization(self) -> None: first = self._register("alpha_user") self._register("beta_user") switched = self.service.switch_account(self.device_a, int(first["user"]["id"])) self.assertEqual(switched["user"]["username"], "alpha_user") remaining = { item["username"] for item in self.service.list_device_accounts(self.device_a)["accounts"] } self.assertEqual(remaining, {"alpha_user", "beta_user"}) def test_switch_without_a_valid_grant_requires_reauthentication(self) -> None: user = self._register("alpha_user") with self.assertRaisesRegex(PermissionError, "该账号需重新验证"): self.service.switch_account(self.device_b, int(user["user"]["id"])) with self.assertRaisesRegex(PermissionError, "该账号需重新验证"): self.service.switch_account("", int(user["user"]["id"])) def test_forget_only_removes_the_current_device_grant(self) -> None: user = self._register("alpha_user") self.service.login("alpha_user", "Password123", self.device_b) self.service.forget_account(self.device_a, int(user["user"]["id"])) self.service.forget_account(self.device_a, int(user["user"]["id"])) self.assertEqual(self.service.list_device_accounts(self.device_a)["accounts"], []) self.assertEqual( self.service.list_device_accounts(self.device_b)["accounts"][0]["username"], "alpha_user", ) def test_logout_revokes_only_the_current_account_on_this_device(self) -> None: first = self._register("alpha_user") second = self._register("beta_user") self.service.revoke_current_device_grant(self.device_a, int(second["user"]["id"])) names = { item["username"] for item in self.service.list_device_accounts(self.device_a)["accounts"] } self.assertEqual(names, {"alpha_user"}) switched = self.service.switch_account(self.device_a, int(first["user"]["id"])) self.assertEqual(switched["user"]["id"], first["user"]["id"]) def test_password_change_revokes_grants_on_every_device(self) -> None: user = self._register("alpha_user") self.service.login("alpha_user", "Password123", self.device_b) self._bind(int(user["user"]["id"])) self.service.change_password("Password123", "Password456") self.assertEqual(self.service.list_device_accounts(self.device_a)["accounts"], []) self.assertEqual(self.service.list_device_accounts(self.device_b)["accounts"], []) with self.assertRaisesRegex(PermissionError, "该账号需重新验证"): self.service.switch_account(self.device_a, int(user["user"]["id"])) def test_device_keeps_at_most_five_accounts(self) -> None: usernames = [f"user_{index}" for index in range(6)] ids = [self._register(name)["user"]["id"] for name in usernames] listed = self.service.list_device_accounts(self.device_a)["accounts"] self.assertEqual(len(listed), 5) kept = {item["user_id"] for item in listed} self.assertNotIn(ids[0], kept) self.assertTrue(set(ids[1:]).issubset(kept)) def test_expired_grants_are_removed_lazily(self) -> None: user = self._register("alpha_user") past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat(timespec="seconds") self.database.upsert_switch_grant( self.device_a, int(user["user"]["id"]), past, past, past, ) self.assertEqual(self.service.list_device_accounts(self.device_a)["accounts"], []) with self.assertRaisesRegex(PermissionError, "该账号需重新验证"): self.service.switch_account(self.device_a, int(user["user"]["id"])) if __name__ == "__main__": unittest.main()