mirror of
https://github.com/halfwaystudent/douyin-sparkflow.git
synced 2026-09-06 16:07:22 +08:00
fix: harden scheduling and deployment flow
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from core import tasks
|
||||
from utils import config as config_module
|
||||
|
||||
|
||||
class ConfigContractTests(unittest.TestCase):
|
||||
def test_default_config_matches_public_example(self):
|
||||
example_path = Path(config_module.__file__).resolve().parents[1] / "config.example.json"
|
||||
example = json.loads(example_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(example, config_module.DEFAULT_CONFIG)
|
||||
|
||||
def test_missing_runtime_config_is_created_from_safe_defaults(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / "config.json"
|
||||
loaded = config_module._load_json_file(path, config_module.DEFAULT_CONFIG)
|
||||
self.assertEqual(config_module.DEFAULT_CONFIG, loaded)
|
||||
self.assertEqual(
|
||||
config_module.DEFAULT_CONFIG,
|
||||
json.loads(path.read_text(encoding="utf-8")),
|
||||
)
|
||||
self.assertFalse(loaded["useProtocolSender"])
|
||||
self.assertTrue(loaded["persistentBrowserProfiles"]["enabled"])
|
||||
|
||||
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)
|
||||
self.assertEqual("/tmp/sparkflow-profiles", normalized["root"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,103 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE_ROOT = REPO_ROOT / "DouYinSparkFlow"
|
||||
|
||||
|
||||
class DeploymentContractTests(unittest.TestCase):
|
||||
def test_github_workflow_is_at_repository_root(self):
|
||||
workflow = REPO_ROOT / ".github" / "workflows" / "schedule.yml"
|
||||
self.assertTrue(workflow.is_file())
|
||||
self.assertFalse((SOURCE_ROOT / ".github" / "workflows" / "schedule.yml").exists())
|
||||
text = workflow.read_text(encoding="utf-8")
|
||||
self.assertIn("working-directory: DouYinSparkFlow", text)
|
||||
self.assertIn("SPARKFLOW_BROWSER_PROFILE_ROOT", text)
|
||||
self.assertIn("SPARKFLOW_MANUAL_RUN", text)
|
||||
self.assertIn("path: DouYinSparkFlow/logs/", text)
|
||||
|
||||
def test_github_actions_are_pinned_to_commit_shas(self):
|
||||
import re
|
||||
|
||||
workflow = (REPO_ROOT / ".github" / "workflows" / "schedule.yml").read_text(encoding="utf-8")
|
||||
uses_values = re.findall(r"^\s*-?\s*uses:\s*([^#\s]+)", workflow, flags=re.MULTILINE)
|
||||
self.assertTrue(uses_values)
|
||||
for value in uses_values:
|
||||
self.assertRegex(value, r"^[^@]+@[0-9a-f]{40}$")
|
||||
|
||||
def test_runtime_config_is_not_tracked_as_the_template(self):
|
||||
self.assertTrue((SOURCE_ROOT / "config.example.json").is_file())
|
||||
self.assertIn("config.json", (SOURCE_ROOT / ".gitignore").read_text(encoding="utf-8"))
|
||||
|
||||
def test_compose_runtime_mounts_follow_least_privilege(self):
|
||||
text = (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
web = text.split(" web:", 1)[1].split("\n login-desktop:", 1)[0]
|
||||
scheduler = text.split(" scheduler:", 1)[1].split("\n task:", 1)[0]
|
||||
task = text.split(" task:", 1)[1]
|
||||
|
||||
self.assertIn("/var/run/docker.sock:/var/run/docker.sock", web)
|
||||
self.assertIn(".:/opt/douyin-sparkflow", web)
|
||||
self.assertIn("/var/run/docker.sock:/var/run/docker.sock", scheduler)
|
||||
self.assertNotIn(".:/opt/douyin-sparkflow", scheduler)
|
||||
self.assertNotIn("/var/run/docker.sock:/var/run/docker.sock", task)
|
||||
self.assertNotIn(".:/opt/douyin-sparkflow", task)
|
||||
for service in (scheduler, task):
|
||||
self.assertIn(
|
||||
"./state/browser-profiles:/opt/douyin-sparkflow/state/browser-profiles",
|
||||
service,
|
||||
)
|
||||
|
||||
def test_cron_reader_accepts_windows_utf8_bom(self):
|
||||
import tempfile
|
||||
|
||||
from scripts.cron_runner import read_crontab
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
path = Path(temp_dir) / "root"
|
||||
path.write_text(
|
||||
"*/20 10-17 * * * cd /app && python main.py --doTask\n",
|
||||
encoding="utf-8-sig",
|
||||
)
|
||||
lines = read_crontab(path)
|
||||
|
||||
self.assertEqual(len(lines), 1)
|
||||
self.assertTrue(lines[0].startswith("*/20 "))
|
||||
|
||||
def test_sensitive_ports_bind_to_loopback_by_default(self):
|
||||
text = (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("${PROXY_BIND_ADDRESS:-127.0.0.1}:${PROXY_HTTP_PORT:-7890}:7890", text)
|
||||
self.assertIn(
|
||||
"${LOGIN_DESKTOP_BIND_ADDRESS:-127.0.0.1}:${LOGIN_DESKTOP_WEB_PORT:-8788}:6080",
|
||||
text,
|
||||
)
|
||||
|
||||
def test_container_login_api_and_public_url_are_wired(self):
|
||||
text = (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("SPARKFLOW_LOGIN_DESKTOP_API_URL: http://login-desktop:18090", text)
|
||||
self.assertIn("SPARKFLOW_LOGIN_DESKTOP_PUBLIC_URL", text)
|
||||
|
||||
def test_installers_preserve_runtime_config_and_do_not_require_bash_on_windows(self):
|
||||
server = (REPO_ROOT / "deploy" / "install-server.sh").read_text(encoding="utf-8")
|
||||
windows = (REPO_ROOT / "deploy" / "install-local.ps1").read_text(encoding="utf-8")
|
||||
self.assertIn("runtime_config_backup", server)
|
||||
self.assertIn("Restored runtime config.json", server)
|
||||
self.assertNotIn("bash ./refresh_proxy.sh", windows)
|
||||
self.assertIn("Initialize-ProxyConfig", windows)
|
||||
|
||||
def test_playwright_base_image_argument_is_used(self):
|
||||
dockerfile = (SOURCE_ROOT / "Dockerfile.server").read_text(encoding="utf-8")
|
||||
self.assertTrue(dockerfile.startswith("ARG PLAYWRIGHT_BASE_IMAGE="))
|
||||
self.assertIn("FROM ${PLAYWRIGHT_BASE_IMAGE}", dockerfile)
|
||||
self.assertIn("docker.io", dockerfile)
|
||||
self.assertIn("node --version", dockerfile)
|
||||
self.assertNotIn("github.com/docker/compose", dockerfile)
|
||||
|
||||
def test_legacy_unused_entrypoints_are_removed(self):
|
||||
self.assertFalse((SOURCE_ROOT / "webui" / "login_sessions.py").exists())
|
||||
self.assertFalse((SOURCE_ROOT / "relogin_worker.py").exists())
|
||||
self.assertFalse((SOURCE_ROOT / "docker-compose.example.yml").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -104,6 +104,53 @@ class WebUiSafetyTests(unittest.TestCase):
|
||||
self.assertEqual(200, response.status_code, path)
|
||||
self.assertEqual("no-store", response.headers["cache-control"])
|
||||
|
||||
def test_login_desktop_urls_honor_container_environment(self):
|
||||
with (
|
||||
patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"SPARKFLOW_LOGIN_DESKTOP_API_URL": "http://login-desktop:18090",
|
||||
"SPARKFLOW_LOGIN_DESKTOP_PUBLIC_URL": "http://127.0.0.1:8788/vnc.html",
|
||||
},
|
||||
),
|
||||
patch.object(app_module, "get_app_settings", return_value={}),
|
||||
):
|
||||
self.assertEqual("http://login-desktop:18090", app_module.login_desktop_api_url())
|
||||
request = type("Request", (), {"url": type("Url", (), {"hostname": "example", "scheme": "http"})()})()
|
||||
self.assertEqual(
|
||||
"http://127.0.0.1:8788/vnc.html",
|
||||
app_module.login_desktop_public_url(request),
|
||||
)
|
||||
|
||||
def test_schedule_sync_writes_configured_window_to_shared_spool(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
cron_path = Path(temp_dir) / "root"
|
||||
with (
|
||||
patch.object(ops, "HOST_CRONTAB_PATH", cron_path),
|
||||
patch.object(ops, "running_in_container", return_value=True),
|
||||
patch.object(ops, "read_crontab", return_value=""),
|
||||
patch.object(
|
||||
ops,
|
||||
"get_config",
|
||||
return_value={
|
||||
"dailySendWindow": {
|
||||
"enabled": True,
|
||||
"startHour": 10,
|
||||
"endHour": 18,
|
||||
"scheduleIntervalMinutes": 20,
|
||||
}
|
||||
},
|
||||
),
|
||||
):
|
||||
result = ops.sync_daily_schedule_from_config()
|
||||
|
||||
self.assertEqual(0, result.returncode)
|
||||
text = cron_path.read_text(encoding="utf-8")
|
||||
self.assertIn("*/20 10-17 * * *", text)
|
||||
self.assertIn("0 18 * * *", text)
|
||||
self.assertIn("20 18 * * *", text)
|
||||
self.assertIn("docker exec", text)
|
||||
|
||||
def test_overview_api_requires_authentication_and_disables_cache(self):
|
||||
client = TestClient(app_module.app)
|
||||
response = client.get("/api/ops/overview")
|
||||
|
||||
Reference in New Issue
Block a user