feat: sync runtime multi-user web and login updates

This commit is contained in:
Rixuan Shao
2026-08-17 23:16:12 +08:00
parent 846ce88daf
commit ee441ff5a1
23 changed files with 1879 additions and 198 deletions
@@ -44,6 +44,11 @@ class ConfigContractTests(unittest.TestCase):
)
self.assertEqual(0, result.returncode, result.stderr)
def test_default_schedule_timezone_resolves_without_fallback(self):
with patch.dict(os.environ, {"SPARKFLOW_TIMEZONE": ""}, clear=False):
schedule_timezone = tasks._schedule_timezone()
self.assertEqual("Asia/Shanghai", getattr(schedule_timezone, "key", None))
def test_profile_root_environment_override_wins(self):
with patch.dict(os.environ, {"SPARKFLOW_BROWSER_PROFILE_ROOT": "/tmp/sparkflow-profiles"}):
normalized = tasks._normalize_persistent_profile_config(config_module.DEFAULT_CONFIG)
+86
View File
@@ -0,0 +1,86 @@
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from webui import login_lock, users
from webui.auth import hash_password
class MultiUserTests(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.users_path = Path(self.temp_dir.name) / "webui_users.json"
self.lock_path = Path(self.temp_dir.name) / "login-workspace.lock.json"
self.accounts = [
{"account_ref": "acc-1", "username": "头像是本人", "unique_id": "111", "targets": [], "enabled": True},
{"account_ref": "acc-2", "username": "你成功捕捉一只野生妖孽", "unique_id": "222", "targets": [], "enabled": True},
{"account_ref": "acc-3", "username": "管理员账号", "unique_id": "333", "targets": [], "enabled": True},
]
self.user_file_patch = patch.object(users, "USERS_FILE", self.users_path)
self.user_file_patch.start()
self.ensure_patch = patch.object(users, "get_userData", return_value=self.accounts)
self.ensure_patch.start()
self.save_accounts_patch = patch.object(users, "save_userData")
self.save_accounts_patch.start()
self.addCleanup(self.ensure_patch.stop)
self.addCleanup(self.save_accounts_patch.stop)
self.addCleanup(self.user_file_patch.stop)
self.addCleanup(self.temp_dir.cleanup)
def test_user_creation_auth_and_unique_assignment(self):
a, changed = users.ensure_account_refs(self.accounts)
self.assertFalse(changed)
ref = a[0]["account_ref"]
created = users.create_web_user("zxb", "zxb123456", account_refs=[ref])
self.assertEqual([ref], created["account_refs"])
identity = users.authenticate("zxb", "zxb123456")
self.assertEqual("user", identity["role"])
self.assertEqual([ref], identity["account_refs"])
self.assertIsNone(users.authenticate("zxb", "wrong"))
with self.assertRaises(users.UserStoreError):
users.create_web_user("zcf", "zcf123456", account_refs=[ref])
def test_visible_accounts_and_admin_reassignment(self):
accounts, _ = users.ensure_account_refs(self.accounts)
first_ref = accounts[0]["account_ref"]
second_ref = accounts[1]["account_ref"]
users.create_web_user("zxb", "secret", account_refs=[first_ref])
principal = {"role": "user", "account_refs": [first_ref]}
self.assertEqual([first_ref], [a["account_ref"] for a in users.get_visible_accounts(principal, accounts)])
users.update_web_user("zxb", account_refs=[second_ref])
self.assertEqual([second_ref], users.find_web_user("zxb")["account_refs"])
self.assertTrue(users.delete_web_user("zxb"))
self.assertEqual([], users.get_web_users())
def test_fifo_queue_promotes_after_active_release(self):
with patch.object(login_lock, "LOCK_PATH", self.lock_path):
first = login_lock.request_workspace(username="zxb", session_id="s1", account_ref="a1", mode="relogin")
second = login_lock.request_workspace(username="zcf", session_id="s2", account_ref="", mode="add")
self.assertEqual("active", first["state"])
self.assertEqual("queued", second["state"])
self.assertEqual("add", second["request"]["mode"])
self.assertEqual(1, second["position"])
self.assertEqual("queued", login_lock.workspace_status(username="zcf", session_id="s2")["state"])
released = login_lock.begin_release(username="zxb", session_id="s1", ticket=first["request"]["ticket"], account_ref="a1")
self.assertIsNotNone(released)
promoted = login_lock.finish_transition()
self.assertEqual("zcf", promoted["username"])
self.assertEqual("active", login_lock.workspace_status(username="zcf", session_id="s2")["state"])
def test_login_workspace_is_serialized_and_expires(self):
with patch.object(login_lock, "LOCK_PATH", self.lock_path), patch.object(login_lock, "LOCK_TTL_SECONDS", 1):
ok, lock = login_lock.acquire(username="zxb", session_id="s1", account_ref="a1")
self.assertTrue(ok)
self.assertTrue(login_lock.owns(lock, username="zxb", session_id="s1", account_ref="a1"))
blocked, current = login_lock.acquire(username="zcf", session_id="s2", account_ref="a2")
self.assertFalse(blocked)
self.assertEqual("zxb", current["username"])
self.assertTrue(login_lock.refresh(username="zxb", session_id="s1", account_ref="a1"))
self.assertTrue(login_lock.release(username="zxb", session_id="s1"))
self.assertIsNone(login_lock.get_lock())
if __name__ == "__main__":
unittest.main()
+34 -2
View File
@@ -11,10 +11,23 @@ from fastapi.testclient import TestClient
from core import tasks
from webui import app as app_module
from webui import login_lock
from webui import ops
class WebUiSafetyTests(unittest.TestCase):
def setUp(self):
try:
login_lock.LOCK_PATH.unlink()
except FileNotFoundError:
pass
def tearDown(self):
try:
login_lock.LOCK_PATH.unlink()
except FileNotFoundError:
pass
def test_windows_invalid_pid_probe_is_treated_as_dead(self):
error = OSError(errno.EINVAL, "invalid pid")
error.winerror = 87
@@ -23,6 +36,19 @@ class WebUiSafetyTests(unittest.TestCase):
with patch.object(tasks.os, "kill", side_effect=error):
self.assertFalse(tasks._pid_is_alive(999999))
def test_missing_optional_runtime_tools_do_not_log_warnings(self):
with (
patch.object(ops.subprocess, "run", side_effect=FileNotFoundError("missing")),
patch.object(ops.logger, "warning") as warning,
patch.object(ops.logger, "debug") as debug,
):
result = ops.run_command(["docker", "ps"])
self.assertEqual(1, result.returncode)
ops.read_crontab()
warning.assert_not_called()
self.assertGreaterEqual(debug.call_count, 2)
def test_stale_lock_inspection_does_not_delete_file(self):
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
@@ -183,6 +209,8 @@ class WebUiSafetyTests(unittest.TestCase):
with (
patch.object(app_module, "current_user", return_value="admin"),
patch.object(app_module, "get_login_lock", return_value={"username": "admin", "session_id": ""}),
patch.object(app_module, "owns_login_lock", return_value=True),
patch.object(
app_module,
"fetch_login_desktop_asset",
@@ -205,6 +233,8 @@ class WebUiSafetyTests(unittest.TestCase):
upstream.read.return_value = b"fake-png"
with (
patch.object(app_module, "current_user", return_value="admin"),
patch.object(app_module, "get_login_lock", return_value={"username": "admin", "session_id": ""}),
patch.object(app_module, "owns_login_lock", return_value=True),
patch.object(app_module.urllib.request, "urlopen", return_value=upstream),
):
response = client.get("/login-desktop/qr")
@@ -225,8 +255,10 @@ class WebUiSafetyTests(unittest.TestCase):
block_start = script.index('document.querySelectorAll(".login-desktop-open")')
block_end = script.index('document.querySelectorAll(".login-desktop-save")', block_start)
block = script[block_start:block_end]
self.assertLess(block.index("window.open(publicUrl"), block.index('await postForm("/login-desktop/open")'))
self.assertIn("refreshLoginQr(1800)", block)
self.assertLess(block.index('window.open("about:blank"'), block.index('postForm("/login-desktop/open"'))
self.assertIn("refreshLoginQr(500)", block)
self.assertIn('data.state === "queued"', block)
self.assertIn("renderWorkspace(data.workspace)", block)
self.assertIn("retries - 1", script)
self.assertIn('/login-desktop/qr/refresh', script)