s
This commit is contained in:
@@ -4,6 +4,7 @@ import base64
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -87,7 +88,6 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertIn("./external-extensions:/usr/share/chromium/extensions:ro", compose)
|
||||
self.assertIn("./external-extensions:/usr/share/chromium-browser/extensions:ro", compose)
|
||||
self.assertIn("./tampermonkey-seed:/etc/neko/tampermonkey-seed:ro", compose)
|
||||
self.assertIn("./youtube-auto-dismiss-extension:/etc/neko/youtube-auto-dismiss-extension:ro", compose)
|
||||
self.assertNotIn("./policies:/etc/chromium/policies/managed", compose)
|
||||
self.assertNotIn("./policies:/etc/opt/chrome/policies/managed", compose)
|
||||
self.assertIn("container_name: live-neko-1", compose)
|
||||
@@ -114,6 +114,11 @@ class NekoConfigTests(unittest.TestCase):
|
||||
{
|
||||
"NEKO_IMAGE": "test-image",
|
||||
"NEKO_PROFILE_HOST_DIR": f"{tmp}/profile",
|
||||
"NEKO_HTTP_PORT_4": "9404",
|
||||
"NEKO_WEBRTC_TCPMUX_4": "54304",
|
||||
"NEKO_WEBRTC_UDPMUX_4": "54305",
|
||||
"NEKO_WEBRTC_UDP_RANGE_4": "54400-54499",
|
||||
"NEKO_PROFILE_HOST_DIR_4": f"{tmp}/custom-profile-4",
|
||||
"NEKO_BROWSER_SUPERVISOR_CONF": "google-chrome.conf",
|
||||
},
|
||||
clear=False,
|
||||
@@ -121,10 +126,11 @@ class NekoConfigTests(unittest.TestCase):
|
||||
rendered = module.render(4)
|
||||
|
||||
self.assertIn("container_name: live-neko-4", rendered)
|
||||
self.assertIn('"9203:8080"', rendered)
|
||||
self.assertIn('"52303:52303/tcp"', rendered)
|
||||
self.assertIn('"52400-52499:52400-52499/udp"', rendered)
|
||||
self.assertIn(f'"{tmp}/profile-4:/home/neko/.config/google-chrome"', rendered)
|
||||
self.assertIn('"9404:8080"', rendered)
|
||||
self.assertIn('"54304:54304/tcp"', rendered)
|
||||
self.assertIn('"54305:54305/udp"', rendered)
|
||||
self.assertIn('"54400-54499:54400-54499/udp"', rendered)
|
||||
self.assertIn(f'"{tmp}/custom-profile-4:/home/neko/.config/google-chrome"', rendered)
|
||||
self.assertIn('NEKO_MANAGED_POLICIES: "0"', rendered)
|
||||
self.assertIn('"./no-managed-policies:/etc/chromium/policies/managed:ro"', rendered)
|
||||
self.assertIn('"./no-managed-policies:/etc/chromium-browser/policies/managed:ro"', rendered)
|
||||
@@ -133,7 +139,6 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertIn('"./external-extensions:/usr/share/chromium/extensions:ro"', rendered)
|
||||
self.assertIn('"./external-extensions:/usr/share/chromium-browser/extensions:ro"', rendered)
|
||||
self.assertIn('"./tampermonkey-seed:/etc/neko/tampermonkey-seed:ro"', rendered)
|
||||
self.assertIn('"./youtube-auto-dismiss-extension:/etc/neko/youtube-auto-dismiss-extension:ro"', rendered)
|
||||
self.assertNotIn('"./policies:/etc/chromium/policies/managed:ro"', rendered)
|
||||
self.assertNotIn('"./policies:/etc/opt/chrome/policies/managed:ro"', rendered)
|
||||
|
||||
@@ -153,9 +158,14 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertIn("compose_file=\"$(render_neko_compose_file)\"", script)
|
||||
self.assertIn("NEKO_MAX_INSTANCE_COUNT:-64", script)
|
||||
self.assertIn("NEKO_MANAGED_POLICIES", script)
|
||||
self.assertIn("copy_neko_tampermonkey_seed_dir()", script)
|
||||
self.assertIn("find_neko_tampermonkey_seed_profile()", script)
|
||||
self.assertIn("services/neko/tampermonkey-seed", script)
|
||||
self.assertIn("sync_neko_tampermonkey_preferences()", script)
|
||||
self.assertIn("should_replace_current", script)
|
||||
self.assertIn('value.get("from_webstore") is not True', script)
|
||||
self.assertIn('path.startswith("/")', script)
|
||||
self.assertIn('value.get("location") == 8', script)
|
||||
self.assertIn("neko_managed_policies_enabled()", script)
|
||||
|
||||
def test_tampermonkey_external_extension_seed_is_available_without_managed_policy(self) -> None:
|
||||
@@ -188,17 +198,14 @@ class NekoConfigTests(unittest.TestCase):
|
||||
launch = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("ensure_tampermonkey_seed", launch)
|
||||
self.assertIn("sync_tampermonkey_preferences", launch)
|
||||
self.assertIn("find_tampermonkey_extension_dir", launch)
|
||||
self.assertIn("find_youtube_auto_dismiss_extension_dir", launch)
|
||||
self.assertIn("--load-extension=$load_extension_dirs", launch)
|
||||
self.assertIn("should_replace_current", launch)
|
||||
self.assertIn('value.get("from_webstore") is not True', launch)
|
||||
self.assertIn('path.startswith("/")', launch)
|
||||
self.assertIn('value.get("location") == 8', launch)
|
||||
self.assertNotIn("--load-extension=", launch)
|
||||
self.assertNotIn("youtube-auto-dismiss-extension", launch)
|
||||
self.assertIn("/etc/neko/tampermonkey-seed", launch)
|
||||
self.assertIn("rm -rf \"$profile_dir/Default/Managed Extension Settings/$ext_id\"", launch)
|
||||
dismiss_manifest = ROOT / "services" / "neko" / "youtube-auto-dismiss-extension" / "manifest.json"
|
||||
self.assertTrue(dismiss_manifest.is_file())
|
||||
dismiss_script = (ROOT / "services" / "neko" / "youtube-auto-dismiss-extension" / "youtube-studio-auto-dismiss.js").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn('text === "Dismiss"', dismiss_script)
|
||||
|
||||
def test_supervisor_uses_browser_launch_script(self) -> None:
|
||||
chrome = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
|
||||
@@ -251,6 +258,46 @@ class NekoConfigTests(unittest.TestCase):
|
||||
self.assertIn("Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo", common)
|
||||
self.assertIn("NEKO_HTTP_PORT_2", common)
|
||||
self.assertIn("NEKO_WEBRTC_UDP_RANGE_3", common)
|
||||
self.assertIn('eval "override=\\"\\${NEKO_PROFILE_HOST_DIR_${idx}:-}\\""', common)
|
||||
|
||||
def test_neko_common_profile_helper_honors_instance_override_beyond_three(self) -> None:
|
||||
script = (
|
||||
f"ROOT_DIR={ROOT!s}; "
|
||||
f"source {ROOT / 'scripts' / 'linux' / 'lib' / 'common.sh'}; "
|
||||
"NEKO_PROFILE_HOST_DIR=/var/lib/live-neko/profile; "
|
||||
"NEKO_PROFILE_HOST_DIR_4=/srv/live/neko/channel-four; "
|
||||
"resolve_neko_profile_dir_for_instance 4"
|
||||
)
|
||||
proc = subprocess.run(["bash", "-lc", script], capture_output=True, text=True, check=False)
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertEqual(proc.stdout.strip(), "/srv/live/neko/channel-four")
|
||||
|
||||
def test_neko_common_arch_selection_covers_x86_and_arm(self) -> None:
|
||||
script = (
|
||||
f"ROOT_DIR={ROOT!s}; "
|
||||
f"source {ROOT / 'scripts' / 'linux' / 'lib' / 'common.sh'}; "
|
||||
"NEKO_IMAGE_X86=ghcr.io/m1k1o/neko/google-chrome:3.0.0; "
|
||||
"NEKO_IMAGE_ARM64=ghcr.io/m1k1o/neko/chromium:3.0.0; "
|
||||
"uname(){ printf '%s\\n' \"$FAKE_UNAME\"; }; "
|
||||
"FAKE_UNAME=x86_64; resolve_neko_image; printf ' '; resolve_neko_supervisor_conf; printf '\\n'; "
|
||||
"FAKE_UNAME=aarch64; resolve_neko_image; printf ' '; resolve_neko_supervisor_conf; printf '\\n'"
|
||||
)
|
||||
proc = subprocess.run(["bash", "-lc", script], capture_output=True, text=True, check=False)
|
||||
self.assertEqual(proc.returncode, 0, proc.stderr)
|
||||
self.assertEqual(
|
||||
proc.stdout.splitlines(),
|
||||
[
|
||||
"ghcr.io/m1k1o/neko/google-chrome:3.0.0 google-chrome.conf",
|
||||
"ghcr.io/m1k1o/neko/chromium:3.0.0 chromium.conf",
|
||||
],
|
||||
)
|
||||
|
||||
def test_neko_install_defaults_match_runtime_screen_profile(self) -> None:
|
||||
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")
|
||||
service_ctl = (ROOT / "scripts" / "linux" / "service_ctl.sh").read_text(encoding="utf-8")
|
||||
self.assertIn('NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"', common)
|
||||
self.assertIn('NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1280x720@15}"', service_ctl)
|
||||
self.assertNotIn('NEKO_DESKTOP_SCREEN="${NEKO_DESKTOP_SCREEN:-1920x1080@30}"', common)
|
||||
|
||||
def test_neko_ip_watcher_is_installed_and_restarts_on_drift(self) -> None:
|
||||
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")
|
||||
|
||||
@@ -89,12 +89,22 @@ class VerifyLocalInstallTests(unittest.TestCase):
|
||||
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
|
||||
if "supervisorctl status google-chrome" in command:
|
||||
return 0, "google-chrome RUNNING pid 8", ""
|
||||
if "NEKO member env invalid" in command:
|
||||
return 0, "provider=multiuser admin_password=set user_password=set\n", ""
|
||||
if "/proc/self/mountinfo" in command:
|
||||
return 0, "/home/neko/.config/chromium\n/home/neko/.config/google-chrome\n", ""
|
||||
if "Developer mode preference not found" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default/Preferences: developer_mode=true\n", ""
|
||||
if "policies/managed" in command and "-name '*.json'" in command:
|
||||
return 0, "", ""
|
||||
if "supervisorctl status tampermonkey-provisioning" in command:
|
||||
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
|
||||
if "tampermonkey-provisioning.json" in command:
|
||||
return 0, "", ""
|
||||
if "Tampermonkey preference not found" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default: state=1 disable_reasons=None\n", ""
|
||||
if "no standalone youtube extension" in command:
|
||||
return 0, "no standalone youtube extension\n", ""
|
||||
if "grep -R -a -l" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default/Local Extension Settings", ""
|
||||
self.fail(f"unexpected docker exec: {container} {command}")
|
||||
@@ -128,8 +138,12 @@ class VerifyLocalInstallTests(unittest.TestCase):
|
||||
"neko_ip_watch_service",
|
||||
"youtube_pro_neko_capacity",
|
||||
"neko_1_browser",
|
||||
"neko_1_multiuser_admin",
|
||||
"neko_1_persistent_profile",
|
||||
"neko_1_developer_mode",
|
||||
"neko_1_personal_policy_mode",
|
||||
"neko_1_tm_payload",
|
||||
"neko_1_tm_enabled",
|
||||
"neko_1_tm_storage",
|
||||
"android_devices",
|
||||
"android_screenshot",
|
||||
@@ -143,12 +157,22 @@ class VerifyLocalInstallTests(unittest.TestCase):
|
||||
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
|
||||
if "supervisorctl status google-chrome" in command:
|
||||
return 0, "google-chrome RUNNING pid 8", ""
|
||||
if "NEKO member env invalid" in command:
|
||||
return 0, "provider=multiuser admin_password=set user_password=set\n", ""
|
||||
if "/proc/self/mountinfo" in command:
|
||||
return 0, "/home/neko/.config/chromium\n/home/neko/.config/google-chrome\n", ""
|
||||
if "Developer mode preference not found" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default/Preferences: developer_mode=true\n", ""
|
||||
if "policies/managed" in command and "-name '*.json'" in command:
|
||||
return 0, "/etc/opt/chrome/policies/managed/policies.json\n", ""
|
||||
if "supervisorctl status tampermonkey-provisioning" in command:
|
||||
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
|
||||
if "tampermonkey-provisioning.json" in command:
|
||||
return 0, "", ""
|
||||
if "Tampermonkey preference not found" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default: state=1 disable_reasons=None\n", ""
|
||||
if "no standalone youtube extension" in command:
|
||||
return 0, "no standalone youtube extension\n", ""
|
||||
if "grep -R -a -l" in command:
|
||||
return 1, "", ""
|
||||
self.fail(f"unexpected docker exec: {container} {command}")
|
||||
@@ -174,12 +198,22 @@ class VerifyLocalInstallTests(unittest.TestCase):
|
||||
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
|
||||
if "supervisorctl status google-chrome" in command:
|
||||
return 0, "google-chrome RUNNING pid 8", ""
|
||||
if "NEKO member env invalid" in command:
|
||||
return 0, "provider=multiuser admin_password=set user_password=set\n", ""
|
||||
if "/proc/self/mountinfo" in command:
|
||||
return 0, "/home/neko/.config/chromium\n/home/neko/.config/google-chrome\n", ""
|
||||
if "Developer mode preference not found" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default/Preferences: developer_mode=true\n", ""
|
||||
if "policies/managed" in command and "-name '*.json'" in command:
|
||||
return 0, "/etc/opt/chrome/policies/managed/policies.json\n", ""
|
||||
if "supervisorctl status tampermonkey-provisioning" in command:
|
||||
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
|
||||
if "tampermonkey-provisioning.json" in command:
|
||||
return 0, "", ""
|
||||
if "Tampermonkey preference not found" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default: state=1 disable_reasons=None\n", ""
|
||||
if "no standalone youtube extension" in command:
|
||||
return 0, "no standalone youtube extension\n", ""
|
||||
if "grep -R -a -l" in command:
|
||||
return 1, "", ""
|
||||
self.fail(f"unexpected docker exec: {container} {command}")
|
||||
@@ -197,6 +231,51 @@ class VerifyLocalInstallTests(unittest.TestCase):
|
||||
self.assertFalse(report["ready"])
|
||||
self.assertEqual(report["failing_checks"][0]["id"], "neko_1_tm_storage")
|
||||
|
||||
def test_neko_tampermonkey_enabled_rejects_unpacked_profile_state(self) -> None:
|
||||
checks: list[dict[str, object]] = []
|
||||
|
||||
def fake_docker_exec(container: str, command: str, *, timeout: float = 8.0):
|
||||
if "supervisorctl status google-chrome" in command:
|
||||
return 0, "google-chrome RUNNING pid 8", ""
|
||||
if "NEKO member env invalid" in command:
|
||||
return 0, "provider=multiuser admin_password=set user_password=set\n", ""
|
||||
if "/proc/self/mountinfo" in command:
|
||||
return 0, "/home/neko/.config/chromium\n/home/neko/.config/google-chrome\n", ""
|
||||
if "Developer mode preference not found" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default/Preferences: developer_mode=true\n", ""
|
||||
if "policies/managed" in command and "-name '*.json'" in command:
|
||||
return 0, "", ""
|
||||
if "supervisorctl status tampermonkey-provisioning" in command:
|
||||
return 0, "tampermonkey-provisioning RUNNING pid 9", ""
|
||||
if "tampermonkey-provisioning.json" in command:
|
||||
return 0, "", ""
|
||||
if "Tampermonkey preference not found" in command:
|
||||
return (
|
||||
1,
|
||||
"/home/neko/.config/google-chrome/Default: "
|
||||
"state=1 disable_reasons=None from_webstore=False "
|
||||
"location=8 path=/etc/neko/tampermonkey-seed/Default/Extensions/dhdgffkkebhmkfjojejmpbldmpobfkfo/5.5.0_0\n",
|
||||
"",
|
||||
)
|
||||
if "no standalone youtube extension" in command:
|
||||
return 0, "no standalone youtube extension\n", ""
|
||||
if "grep -R -a -l" in command:
|
||||
return 0, "/home/neko/.config/google-chrome/Default/Local Extension Settings", ""
|
||||
self.fail(f"unexpected docker exec: {container} {command}")
|
||||
|
||||
with patch.object(verify_local_install, "docker_ps_names", return_value=(True, ["live-neko-1", "live-neko-2"], "")):
|
||||
with patch.object(verify_local_install, "systemd_active", return_value=(True, "active")):
|
||||
with patch.object(verify_local_install, "docker_exec", side_effect=fake_docker_exec):
|
||||
verify_local_install.verify_neko(
|
||||
checks,
|
||||
{"ENABLE_NEKO": "1", "NEKO_INSTANCE_COUNT": "2", "NEKO_MANAGED_POLICIES": "0"},
|
||||
timeout=5.0,
|
||||
)
|
||||
|
||||
report = verify_local_install.summarize(checks)
|
||||
self.assertFalse(report["ready"])
|
||||
self.assertEqual(report["failing_checks"][0]["id"], "neko_1_tm_enabled")
|
||||
|
||||
def test_android_endpoint_without_devices_is_not_a_required_install_failure(self) -> None:
|
||||
checks: list[dict[str, object]] = []
|
||||
with patch.object(
|
||||
|
||||
@@ -43,17 +43,19 @@ class WebDynamicProcessesTests(unittest.TestCase):
|
||||
def test_script_for_pm2_or_pro_accepts_custom_saved_pm2_name(self) -> None:
|
||||
channels = [{"id": "lane3", "name": "Lane 3", "pm2Name": "custom-lane-three"}]
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
resolved = web.script_for_pm2_or_pro("custom-lane-three")
|
||||
resolved = web.script_for_pm2_or_pro("youtube2__custom-lane-three")
|
||||
|
||||
self.assertEqual(resolved, "youtube.py")
|
||||
|
||||
def test_process_monitor_entries_keep_static_youtube_label_for_lane_one(self) -> None:
|
||||
def test_process_monitor_entries_migrates_legacy_youtube_pm2_without_hiding_single_lane(self) -> None:
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube"}]
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
entries = web.process_monitor_entries()
|
||||
|
||||
youtube_row = next(row for row in entries if row["pm2"] == "youtube")
|
||||
self.assertEqual(youtube_row["label"], "YouTube")
|
||||
pro_row = next(row for row in entries if row["pm2"] == "youtube2__lane1")
|
||||
self.assertEqual(pro_row["label"], "YouTube Pro · Lane 1")
|
||||
|
||||
def test_process_monitor_entries_do_not_duplicate_static_pm2_rows(self) -> None:
|
||||
channels = [
|
||||
@@ -64,6 +66,7 @@ class WebDynamicProcessesTests(unittest.TestCase):
|
||||
entries = web.process_monitor_entries()
|
||||
|
||||
self.assertEqual(sum(1 for row in entries if row["pm2"] == "youtube"), 1)
|
||||
self.assertEqual(sum(1 for row in entries if row["pm2"] == "youtube2__lane1"), 1)
|
||||
self.assertEqual(sum(1 for row in entries if row["pm2"] == "youtube2__lane2"), 1)
|
||||
|
||||
def test_web_module_does_not_repeat_process_entry_function_defs(self) -> None:
|
||||
@@ -183,6 +186,44 @@ class WebDynamicProcessesTests(unittest.TestCase):
|
||||
self.assertEqual(payload.headers.get("cache-control"), "no-store, max-age=0, must-revalidate")
|
||||
|
||||
def test_save_url_config_updates_matching_youtube_pro_channel_store(self) -> None:
|
||||
captured: list[list[dict[str, object]]] = []
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1", "urlLines": "old\n"}]
|
||||
|
||||
with patch("web.allows_url_config", return_value=True):
|
||||
with patch("web.managed_url_config_relpath", return_value="URL_config.youtube2__lane1.ini"):
|
||||
with patch("web.governed_save_managed_file"):
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
|
||||
payload = asyncio.run(
|
||||
web.save_url_config(
|
||||
_JsonRequest({"content": "https://live.douyin.com/fresh\n"}),
|
||||
process="youtube2__lane1",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
|
||||
self.assertEqual(captured[-1][0]["urlLines"], "https://live.douyin.com/fresh\n")
|
||||
|
||||
def test_save_config_updates_matching_youtube_pro_channel_stream_key(self) -> None:
|
||||
captured: list[list[dict[str, object]]] = []
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1", "streamKey": "old-key"}]
|
||||
content = "[youtube]\nkey = new-key-123\nrtmps = 是\n"
|
||||
|
||||
with patch("web.managed_youtube_ini_relpath", return_value="youtube.youtube2__lane1.ini"):
|
||||
with patch("web.governed_save_managed_file"):
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
|
||||
payload = asyncio.run(
|
||||
web.save_config(
|
||||
_JsonRequest({"content": content}),
|
||||
process="youtube2__lane1",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
|
||||
self.assertEqual(captured[-1][0]["streamKey"], "new-key-123")
|
||||
|
||||
def test_single_youtube_save_does_not_mutate_legacy_pro_channel_named_youtube(self) -> None:
|
||||
captured: list[list[dict[str, object]]] = []
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube", "urlLines": "old\n"}]
|
||||
|
||||
@@ -193,32 +234,13 @@ class WebDynamicProcessesTests(unittest.TestCase):
|
||||
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
|
||||
payload = asyncio.run(
|
||||
web.save_url_config(
|
||||
_JsonRequest({"content": "https://live.douyin.com/fresh\n"}),
|
||||
_JsonRequest({"content": "https://live.douyin.com/single\n"}),
|
||||
process="youtube",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
|
||||
self.assertEqual(captured[-1][0]["urlLines"], "https://live.douyin.com/fresh\n")
|
||||
|
||||
def test_save_config_updates_matching_youtube_pro_channel_stream_key(self) -> None:
|
||||
captured: list[list[dict[str, object]]] = []
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube", "streamKey": "old-key"}]
|
||||
content = "[youtube]\nkey = new-key-123\nrtmps = 是\n"
|
||||
|
||||
with patch("web.managed_youtube_ini_relpath", return_value="youtube.youtube.ini"):
|
||||
with patch("web.governed_save_managed_file"):
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
|
||||
payload = asyncio.run(
|
||||
web.save_config(
|
||||
_JsonRequest({"content": content}),
|
||||
process="youtube",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
|
||||
self.assertEqual(captured[-1][0]["streamKey"], "new-key-123")
|
||||
self.assertEqual(captured, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -17,6 +17,10 @@ class YoutubeProRuntimeTests(unittest.TestCase):
|
||||
def test_normalize_channels_assigns_default_pm2_and_rejects_duplicate_names(self) -> None:
|
||||
rows = normalize_youtube_pro_channels([{"id": "lane2", "name": "Lane 2"}])
|
||||
self.assertEqual(rows[0]["pm2Name"], "youtube2__lane2")
|
||||
rows = normalize_youtube_pro_channels([{"id": "lane1", "pm2Name": "youtube"}])
|
||||
self.assertEqual(rows[0]["pm2Name"], "youtube2__lane1")
|
||||
rows = normalize_youtube_pro_channels([{"id": "lane3", "pm2Name": "custom"}])
|
||||
self.assertEqual(rows[0]["pm2Name"], "youtube2__custom")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
normalize_youtube_pro_channels(
|
||||
|
||||
Reference in New Issue
Block a user