feat: token-auth removal, security hardening, Stripe-style compat layer (v0.9.2)
- Remove token auth entirely; only Ed25519 pubkey auth or --no-auth - Add 32 MB message-size cap in serve and client (DoS protection) - Set Unix socket to 0o600 after bind in native_host (multi-user hardening) - Enforce browser-cli/VERSION user-agent on all TCP connections - Add PROTOCOL_MIN_CLIENT check (>= 0.9.0) server- and client-side - Include server_version + min_client_version in challenge frame - Add browser_cli/version_manager.py: parse_version, get_installed_version - Add browser_cli/compat.py: Stripe-style versioning layer with adapt_request / adapt_response hooks; baseline 0.9.2, no shims needed yet - Fix BrowserCLI key handling: no Path() wrap for agent specs - Fix _multi_browser_targets() to forward key to remote_browser_targets() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+46
-47
@@ -65,10 +65,9 @@ class TestBrowserCLIInit:
|
||||
assert b._browser == "chrome"
|
||||
|
||||
def test_remote_options_stored(self):
|
||||
b = BrowserCLI(browser="work", remote="host:8765", token="secret", key=None)
|
||||
b = BrowserCLI(browser="work", remote="host:8765", key=None)
|
||||
assert b._browser == "work"
|
||||
assert b._remote == "host:8765"
|
||||
assert b._token == "secret"
|
||||
|
||||
|
||||
# ── Internal factories ────────────────────────────────────────────────────────
|
||||
@@ -129,7 +128,7 @@ class TestNavigation:
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.open",
|
||||
{"url": "https://example.com", "background": False, "window": None, "group": None},
|
||||
profile=None, remote=None, token=None, key=None,
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_open_background(self, b, mock_send):
|
||||
@@ -143,38 +142,38 @@ class TestNavigation:
|
||||
|
||||
def test_reload(self, b, mock_send):
|
||||
b.reload(tab_id=5)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 5}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_hard_reload(self, b, mock_send):
|
||||
b.hard_reload(tab_id=7)
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 7}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_back(self, b, mock_send):
|
||||
b.back(tab_id=3)
|
||||
mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("navigate.back", {"tabId": 3}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_forward(self, b, mock_send):
|
||||
b.forward(tab_id=3)
|
||||
mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("navigate.forward", {"tabId": 3}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_focus_url(self, b, mock_send):
|
||||
b.focus_url("github.com")
|
||||
mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("navigate.focus", {"pattern": "github.com"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_navigate_tab(self, b, mock_send):
|
||||
b.navigate_tab(5, "https://example.com")
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None, remote=None, token=None, key=None
|
||||
"navigate.to", {"tabId": 5, "url": "https://example.com"}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
def test_profile_forwarded(self, b_profile, mock_send):
|
||||
b_profile.reload()
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="brave", remote=None, key=None)
|
||||
|
||||
def test_remote_forwarded(self, mock_send):
|
||||
b = BrowserCLI(browser="work", remote="host:8765", token="secret", key=None)
|
||||
b = BrowserCLI(browser="work", remote="host:8765", key=None)
|
||||
b.reload()
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="work", remote="host:8765", token="secret", key=None)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": None}, profile="work", remote="host:8765", key=None)
|
||||
|
||||
|
||||
# ── Search ────────────────────────────────────────────────────────────────────
|
||||
@@ -207,12 +206,12 @@ class TestExtract:
|
||||
result = b.extract_markdown()
|
||||
|
||||
assert result == "# Title"
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_extract_markdown_selector(self, b, mock_send):
|
||||
b.extract_markdown("article")
|
||||
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
|
||||
|
||||
|
||||
# ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||
@@ -247,7 +246,7 @@ class TestTabs:
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.close",
|
||||
{"tabId": 10, "inactive": False, "duplicates": False},
|
||||
profile=None, remote=None, token=None, key=None,
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_tabs_move(self, b, mock_send):
|
||||
@@ -255,19 +254,19 @@ class TestTabs:
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.move",
|
||||
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
||||
profile=None, remote=None, token=None, key=None,
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_tabs_active(self, b, mock_send):
|
||||
b.tabs_active(10)
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_window_active_tab(self, b, mock_send):
|
||||
mock_send.return_value = TAB_DATA
|
||||
tab = b.window_active_tab(1)
|
||||
assert isinstance(tab, Tab)
|
||||
assert tab.id == 10
|
||||
mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("tabs.active_in_window", {"windowId": 1}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_window_active_tab_missing_raises(self, b, mock_send):
|
||||
mock_send.return_value = None
|
||||
@@ -319,17 +318,17 @@ class TestTabs:
|
||||
assert mock_send.call_args_list == [
|
||||
call("tabs.list", {}, profile="default"),
|
||||
call("tabs.list", {}, profile="work"),
|
||||
call("tabs.close", {"tabId": 11}, profile="work", remote=None, token=None, key=None),
|
||||
call("tabs.close", {"tabId": 11}, profile="work", remote=None, key=None),
|
||||
]
|
||||
|
||||
def test_tabs_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send):
|
||||
b = BrowserCLI(remote="host:8765", token="secret", key=None)
|
||||
b = BrowserCLI(remote="host:8765", key=None)
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
side_effect=AssertionError("local targets should not be used for explicit remote"),
|
||||
), patch(
|
||||
"browser_cli.remote_browser_targets",
|
||||
return_value=[BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")],
|
||||
return_value=[BrowserTarget("work", "host:work", "", remote="host:8765")],
|
||||
):
|
||||
mock_send.side_effect = [[TAB_DATA], None]
|
||||
tabs = b.tabs_list()
|
||||
@@ -337,8 +336,8 @@ class TestTabs:
|
||||
|
||||
assert [tab.browser for tab in tabs] == ["host:work"]
|
||||
assert mock_send.call_args_list == [
|
||||
call("tabs.list", {}, profile="work", remote="host:8765", token="secret", key=None),
|
||||
call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", token="secret", key=None),
|
||||
call("tabs.list", {}, profile="work", remote="host:8765", key=None),
|
||||
call("tabs.close", {"tabId": 10}, profile="work", remote="host:8765", key=None),
|
||||
]
|
||||
|
||||
def test_tabs_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||
@@ -381,7 +380,7 @@ class TestTabs:
|
||||
|
||||
def test_tabs_sort(self, b, mock_send):
|
||||
b.tabs_sort(by="title")
|
||||
mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("tabs.sort", {"by": "title"}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_tabs_merge_windows(self, b, mock_send):
|
||||
mock_send.return_value = {"moved": 4}
|
||||
@@ -414,7 +413,7 @@ class TestGroups:
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
tabs = b.group_tabs(42)
|
||||
assert isinstance(tabs[0], Tab)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_group_count(self, b, mock_send):
|
||||
mock_send.return_value = 7
|
||||
@@ -442,17 +441,17 @@ class TestGroups:
|
||||
assert mock_send.call_args_list == [
|
||||
call("group.list", {}, profile="default"),
|
||||
call("group.list", {}, profile="work"),
|
||||
call("group.close", {"groupId": 99}, profile="work", remote=None, token=None, key=None),
|
||||
call("group.close", {"groupId": 99}, profile="work", remote=None, key=None),
|
||||
]
|
||||
|
||||
def test_group_list_remote_uses_only_requested_remote_and_binds_actions(self, mock_send):
|
||||
b = BrowserCLI(remote="host:8765", token="secret", key=None)
|
||||
b = BrowserCLI(remote="host:8765", key=None)
|
||||
with patch(
|
||||
"browser_cli.active_browser_targets",
|
||||
side_effect=AssertionError("local targets should not be used for explicit remote"),
|
||||
), patch(
|
||||
"browser_cli.remote_browser_targets",
|
||||
return_value=[BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")],
|
||||
return_value=[BrowserTarget("work", "host:work", "", remote="host:8765")],
|
||||
):
|
||||
mock_send.side_effect = [[GROUP_DATA], None]
|
||||
groups = b.group_list()
|
||||
@@ -460,8 +459,8 @@ class TestGroups:
|
||||
|
||||
assert [group.browser for group in groups] == ["host:work"]
|
||||
assert mock_send.call_args_list == [
|
||||
call("group.list", {}, profile="work", remote="host:8765", token="secret", key=None),
|
||||
call("group.close", {"groupId": 42}, profile="work", remote="host:8765", token="secret", key=None),
|
||||
call("group.list", {}, profile="work", remote="host:8765", key=None),
|
||||
call("group.close", {"groupId": 42}, profile="work", remote="host:8765", key=None),
|
||||
]
|
||||
|
||||
def test_group_count_multi_browser_returns_browser_counts(self, b, mock_send):
|
||||
@@ -484,7 +483,7 @@ class TestGroups:
|
||||
|
||||
def test_group_close(self, b, mock_send):
|
||||
b.group_close(42)
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_group_create_dict_response(self, b, mock_send):
|
||||
mock_send.return_value = GROUP_DATA
|
||||
@@ -504,7 +503,7 @@ class TestGroups:
|
||||
tab_id = b.group_add_tab(42, "https://example.com")
|
||||
assert tab_id == 55
|
||||
mock_send.assert_called_once_with(
|
||||
"group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None, remote=None, token=None, key=None
|
||||
"group.add_tab", {"group": "42", "url": "https://example.com"}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
def test_group_add_tab_non_dict_response(self, b, mock_send):
|
||||
@@ -514,7 +513,7 @@ class TestGroups:
|
||||
def test_group_move_forward(self, b, mock_send):
|
||||
b.group_move(42, forward=True)
|
||||
mock_send.assert_called_once_with(
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None, key=None
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
|
||||
@@ -544,7 +543,7 @@ class TestWindows:
|
||||
result = b.windows_open()
|
||||
|
||||
assert result == {"id": 5}
|
||||
mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("windows.open", {"url": None}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_windows_open_with_url(self, b, mock_send):
|
||||
mock_send.return_value = {"id": 9}
|
||||
@@ -552,7 +551,7 @@ class TestWindows:
|
||||
result = b.windows_open("https://example.com")
|
||||
|
||||
assert result == {"id": 9}
|
||||
mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, key=None)
|
||||
|
||||
|
||||
class TestSession:
|
||||
@@ -562,7 +561,7 @@ class TestSession:
|
||||
result = b.session_list()
|
||||
|
||||
assert result == [{"name": "saved", "tabs": 3, "savedAt": 1712707200000}]
|
||||
mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("session.list", {}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_session_list_multi_browser_adds_browser(self, b, mock_send):
|
||||
with patch(
|
||||
@@ -597,26 +596,26 @@ class TestTabModel:
|
||||
|
||||
def test_close(self, tab, mock_send):
|
||||
tab.close()
|
||||
mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("tabs.close", {"tabId": 10}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_activate(self, tab, mock_send):
|
||||
tab.activate()
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("tabs.active", {"tabId": 10}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_reload(self, tab, mock_send):
|
||||
tab.reload()
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("navigate.reload", {"tabId": 10}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_hard_reload(self, tab, mock_send):
|
||||
tab.hard_reload()
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("navigate.hard_reload", {"tabId": 10}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_move_forward(self, tab, mock_send):
|
||||
tab.move(forward=True)
|
||||
mock_send.assert_called_once_with(
|
||||
"tabs.move",
|
||||
{"tabId": 10, "forward": True, "backward": False, "groupId": None, "windowId": None, "index": None},
|
||||
profile=None, remote=None, token=None, key=None,
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_move_to_group(self, tab, mock_send):
|
||||
@@ -626,12 +625,12 @@ class TestTabModel:
|
||||
def test_html(self, tab, mock_send):
|
||||
mock_send.return_value = "<html/>"
|
||||
assert tab.html() == "<html/>"
|
||||
mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("tabs.html", {"tabId": 10}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_open(self, tab, mock_send):
|
||||
tab.open("https://new.example.com")
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, token=None, key=None
|
||||
"navigate.to", {"tabId": 10, "url": "https://new.example.com"}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
def test_open_background_changes_same_tab(self, tab, mock_send):
|
||||
@@ -639,7 +638,7 @@ class TestTabModel:
|
||||
mock_send.assert_called_once_with(
|
||||
"navigate.to",
|
||||
{"tabId": 10, "url": "https://new.example.com"},
|
||||
profile=None, remote=None, token=None, key=None,
|
||||
profile=None, remote=None, key=None,
|
||||
)
|
||||
|
||||
def test_unbound_raises(self):
|
||||
@@ -657,18 +656,18 @@ class TestGroupModel:
|
||||
|
||||
def test_close(self, group, mock_send):
|
||||
group.close()
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("group.close", {"groupId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_tabs(self, group, mock_send):
|
||||
mock_send.return_value = [TAB_DATA]
|
||||
tabs = group.tabs()
|
||||
assert isinstance(tabs[0], Tab)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, token=None, key=None)
|
||||
mock_send.assert_called_once_with("group.tabs", {"groupId": 42}, profile=None, remote=None, key=None)
|
||||
|
||||
def test_move_forward(self, group, mock_send):
|
||||
group.move(forward=True)
|
||||
mock_send.assert_called_once_with(
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, token=None, key=None
|
||||
"group.move", {"group": "42", "forward": True, "backward": False}, profile=None, remote=None, key=None
|
||||
)
|
||||
|
||||
def test_move_backward(self, group, mock_send):
|
||||
|
||||
+10
-24
@@ -168,19 +168,16 @@ def test_clients_reads_registry_with_trailing_garbage(tmp_path):
|
||||
assert "0.8.2" in result.output
|
||||
|
||||
def test_clients_remote_uses_remote_endpoint_without_local_registry():
|
||||
def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None):
|
||||
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
|
||||
assert command == "clients.list"
|
||||
assert profile is None
|
||||
assert remote == "127.0.0.1:8765"
|
||||
assert token == "test"
|
||||
return [{"name": "Chrome", "version": "1", "extensionVersion": "2.3.4"}]
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True), patch(
|
||||
"browser_cli.cli.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json")
|
||||
), patch("browser_cli.cli.send_command", side_effect=fake_send_command) as send_command, patch(
|
||||
"browser_cli.cli.save_remote_token"
|
||||
):
|
||||
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--token", "test", "clients"])
|
||||
), patch("browser_cli.cli.send_command", side_effect=fake_send_command) as send_command:
|
||||
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "clients"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
send_command.assert_called_once()
|
||||
@@ -194,7 +191,7 @@ def test_clients_remote_respects_global_browser_route():
|
||||
result = CliRunner().invoke(main, ["--remote", "127.0.0.1:8765", "--browser", "work", "clients"])
|
||||
|
||||
assert result.exit_code == 1
|
||||
send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", token=None, key=None)
|
||||
send_command.assert_called_once_with("clients.list", profile="work", remote="127.0.0.1:8765", key=None)
|
||||
|
||||
|
||||
def test_clients_browser_alias_resolves_to_remote():
|
||||
@@ -207,15 +204,13 @@ def test_clients_browser_alias_resolves_to_remote():
|
||||
display_name="192.168.188.104:automatisation",
|
||||
socket_path="",
|
||||
remote="192.168.188.104:8765",
|
||||
token="tok",
|
||||
)
|
||||
all_remote_targets = [resolved_target]
|
||||
|
||||
def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None):
|
||||
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
|
||||
assert command == "clients.list"
|
||||
assert profile == "automatisation"
|
||||
assert remote == "192.168.188.104:8765"
|
||||
assert token == "tok"
|
||||
return [{"name": "Chrome", "version": "147.0.0.0", "extensionVersion": "0.8.5"}]
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True), patch(
|
||||
@@ -287,17 +282,17 @@ def test_tabs_list_with_remote_uses_only_remote_targets():
|
||||
side_effect=AssertionError("local targets should not be used for explicit remote"),
|
||||
), patch(
|
||||
"browser_cli.commands.tabs.remote_browser_targets",
|
||||
return_value=[BrowserTarget("work", "remote-host:work", "", remote="remote-host:8765", token="secret")],
|
||||
return_value=[BrowserTarget("work", "remote-host:work", "", remote="remote-host:8765")],
|
||||
), patch(
|
||||
"browser_cli.commands.tabs.send_command",
|
||||
return_value=[{"id": 1, "windowId": 1, "active": True, "title": "Remote", "url": "https://example.com"}],
|
||||
) as send_command, patch("browser_cli.cli.save_remote_token"):
|
||||
result = CliRunner().invoke(main, ["--remote", "remote-host:8765", "--token", "secret", "tabs", "list"])
|
||||
) as send_command:
|
||||
result = CliRunner().invoke(main, ["--remote", "remote-host:8765", "tabs", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "remote-host:work" in result.output
|
||||
assert "Remote" in result.output
|
||||
send_command.assert_called_once_with("tabs.list", {}, profile="work", remote="remote-host:8765", token="secret")
|
||||
send_command.assert_called_once_with("tabs.list", {}, profile="work", remote="remote-host:8765")
|
||||
|
||||
|
||||
def test_tabs_list_with_explicit_browser_does_not_show_browser_column():
|
||||
@@ -634,14 +629,6 @@ def test_convert_html_to_markdown_indents_multiline_list_items():
|
||||
" Local LLMs / API Modelle / Spezialmodelle"
|
||||
) in markdown
|
||||
|
||||
def test_remote_token_is_saved_when_passed_on_cli():
|
||||
endpoint = "browser-host.example:8765"
|
||||
with patch("browser_cli.cli.save_remote_token") as save_remote_token:
|
||||
result = CliRunner().invoke(main, ["--remote", endpoint, "--token", "secret", "completion", "bash", "--script"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
save_remote_token.assert_called_once_with(endpoint, "secret")
|
||||
|
||||
|
||||
def test_tabs_list_multi_browser_queries_remote_target():
|
||||
endpoint = "browser-host.example:8765"
|
||||
@@ -650,7 +637,6 @@ def test_tabs_list_multi_browser_queries_remote_target():
|
||||
"browser-host.example:work",
|
||||
"",
|
||||
remote=endpoint,
|
||||
token="secret",
|
||||
)
|
||||
|
||||
with patch("browser_cli.commands.tabs.active_browser_targets", return_value=[remote_target, BrowserTarget("local", "local", "/tmp/local.sock")]), patch(
|
||||
@@ -660,5 +646,5 @@ def test_tabs_list_multi_browser_queries_remote_target():
|
||||
result = CliRunner().invoke(main, ["tabs", "list"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
send_command.assert_any_call("tabs.list", {}, profile="work", remote=endpoint, token="secret")
|
||||
send_command.assert_any_call("tabs.list", {}, profile="work", remote=endpoint)
|
||||
assert "browser-host.example:work" in result.output
|
||||
|
||||
+18
-39
@@ -10,10 +10,8 @@ from browser_cli.client import (
|
||||
active_browser_targets,
|
||||
display_browser_name,
|
||||
key_for_remote,
|
||||
save_remote_token,
|
||||
send_command,
|
||||
remote_target_for_alias,
|
||||
token_for_remote,
|
||||
)
|
||||
from browser_cli.platform import endpoint_for_alias
|
||||
|
||||
@@ -94,29 +92,15 @@ def test_active_browser_targets_keeps_windows_registry_entries(monkeypatch, tmp_
|
||||
assert targets[0].socket_path == r"\\.\pipe\browser-cli-work"
|
||||
|
||||
|
||||
def test_save_remote_token_persists_per_endpoint(monkeypatch, tmp_path):
|
||||
remotes_path = tmp_path / "remotes.json"
|
||||
monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_path)
|
||||
|
||||
endpoint = "browser-host.example:8765"
|
||||
|
||||
save_remote_token(endpoint, "secret-token")
|
||||
|
||||
assert token_for_remote(endpoint) == "secret-token"
|
||||
assert json.loads(remotes_path.read_text(encoding="utf-8")) == {
|
||||
endpoint: {"token": "secret-token"}
|
||||
}
|
||||
|
||||
|
||||
def test_send_command_auto_routes_single_remote_target(monkeypatch):
|
||||
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False)
|
||||
sent = {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"browser_cli.client.remote_browser_targets",
|
||||
lambda endpoint, token=None, key=None: [BrowserTarget("work", "host:work", "", remote=endpoint, token=token)],
|
||||
lambda endpoint, key=None: [BrowserTarget("work", "host:work", "", remote=endpoint)],
|
||||
)
|
||||
|
||||
def fake_send_remote(endpoint, msg, private_key=None):
|
||||
@@ -125,20 +109,19 @@ def test_send_command_auto_routes_single_remote_target(monkeypatch):
|
||||
|
||||
monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote)
|
||||
|
||||
assert send_command("tabs.list", remote="host:8765", token="secret", key=None) == "ok"
|
||||
assert send_command("tabs.list", remote="host:8765", key=None) == "ok"
|
||||
assert sent["_route"] == "work"
|
||||
assert sent["token"] == "secret"
|
||||
assert "token" not in sent
|
||||
|
||||
|
||||
def test_send_command_resolves_browser_alias_to_remote_target(monkeypatch):
|
||||
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False)
|
||||
monkeypatch.setenv("BROWSER_CLI_PROFILE", "host:work")
|
||||
sent = {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"browser_cli.client._remote_browser_targets",
|
||||
lambda: [BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")],
|
||||
lambda: [BrowserTarget("work", "host:work", "", remote="host:8765")],
|
||||
)
|
||||
|
||||
def fake_send_remote(endpoint, msg, private_key=None):
|
||||
@@ -151,13 +134,13 @@ def test_send_command_resolves_browser_alias_to_remote_target(monkeypatch):
|
||||
assert send_command("tabs.list") == []
|
||||
assert sent["endpoint"] == "host:8765"
|
||||
assert sent["_route"] == "work"
|
||||
assert sent["token"] == "secret"
|
||||
assert "token" not in sent
|
||||
|
||||
|
||||
def test_remote_target_for_alias_accepts_full_endpoint_profile(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"browser_cli.client._remote_browser_targets",
|
||||
lambda: [BrowserTarget("work", "host:work", "", remote="host:8765", token="secret")],
|
||||
lambda: [BrowserTarget("work", "host:work", "", remote="host:8765")],
|
||||
)
|
||||
|
||||
target = remote_target_for_alias("host:8765:work")
|
||||
@@ -172,7 +155,7 @@ def test_remote_target_for_alias_accepts_host_when_only_one_remote_target(monkey
|
||||
remote_endpoint = f"{remote_host}:8765"
|
||||
monkeypatch.setattr(
|
||||
"browser_cli.client._remote_browser_targets",
|
||||
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint, token="secret")],
|
||||
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint)],
|
||||
)
|
||||
|
||||
target = remote_target_for_alias(remote_host)
|
||||
@@ -186,13 +169,12 @@ def test_send_command_resolves_host_alias_to_single_remote_target(monkeypatch):
|
||||
remote_host = "browser-host.example"
|
||||
remote_endpoint = f"{remote_host}:8765"
|
||||
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False)
|
||||
monkeypatch.setenv("BROWSER_CLI_PROFILE", remote_host)
|
||||
sent = {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"browser_cli.client._remote_browser_targets",
|
||||
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint, token="secret")],
|
||||
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint)],
|
||||
)
|
||||
|
||||
def fake_send_remote(endpoint, msg, private_key=None):
|
||||
@@ -205,15 +187,15 @@ def test_send_command_resolves_host_alias_to_single_remote_target(monkeypatch):
|
||||
assert send_command("tabs.list") == []
|
||||
assert sent["endpoint"] == remote_endpoint
|
||||
assert sent["_route"] == "work"
|
||||
assert sent["token"] == "secret"
|
||||
assert "token" not in sent
|
||||
|
||||
|
||||
def test_remote_target_for_alias_keeps_host_alias_ambiguous_for_multiple_targets(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"browser_cli.client._remote_browser_targets",
|
||||
lambda: [
|
||||
BrowserTarget("main", "host:main", "", remote="host:8765", token="secret"),
|
||||
BrowserTarget("work", "host:work", "", remote="host:8765", token="secret"),
|
||||
BrowserTarget("main", "host:main", "", remote="host:8765"),
|
||||
BrowserTarget("work", "host:work", "", remote="host:8765"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -224,27 +206,26 @@ def test_send_command_requires_browser_for_multiple_remote_targets(monkeypatch):
|
||||
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
|
||||
monkeypatch.setattr(
|
||||
"browser_cli.client.remote_browser_targets",
|
||||
lambda endpoint, token=None, key=None: [
|
||||
BrowserTarget("main", "host:main", "", remote=endpoint, token=token),
|
||||
BrowserTarget("furry", "host:furry", "", remote=endpoint, token=token),
|
||||
lambda endpoint, key=None: [
|
||||
BrowserTarget("main", "host:main", "", remote=endpoint),
|
||||
BrowserTarget("furry", "host:furry", "", remote=endpoint),
|
||||
],
|
||||
)
|
||||
|
||||
with pytest.raises(BrowserNotConnected, match="Multiple remote browser instances are active: main, furry"):
|
||||
send_command("tabs.list", remote="host:8765", token="secret")
|
||||
send_command("tabs.list", remote="host:8765")
|
||||
|
||||
|
||||
def test_active_browser_targets_includes_remote_targets(monkeypatch, tmp_path):
|
||||
remotes_path = tmp_path / "remotes.json"
|
||||
endpoint = "browser-host.example:8765"
|
||||
remotes_path.write_text(json.dumps({endpoint: {"token": "secret-token"}}), encoding="utf-8")
|
||||
remotes_path.write_text(json.dumps({endpoint: {}}), encoding="utf-8")
|
||||
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", tmp_path / "missing-registry.json")
|
||||
monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_path)
|
||||
|
||||
def fake_send_command(command, args=None, profile=None, remote=None, token=None, key=None):
|
||||
def fake_send_command(command, args=None, profile=None, remote=None, key=None):
|
||||
assert command == "browser-cli.targets"
|
||||
assert remote == endpoint
|
||||
assert token == "secret-token"
|
||||
return [{"profile": "work", "displayName": "work"}]
|
||||
|
||||
monkeypatch.setattr("browser_cli.client.send_command", fake_send_command)
|
||||
@@ -255,7 +236,6 @@ def test_active_browser_targets_includes_remote_targets(monkeypatch, tmp_path):
|
||||
assert targets[0].profile == "work"
|
||||
assert targets[0].display_name == "browser-host.example:work"
|
||||
assert targets[0].remote == endpoint
|
||||
assert targets[0].token == "secret-token"
|
||||
|
||||
|
||||
def test_send_command_auto_saves_and_reuses_key_for_remote(monkeypatch, tmp_path):
|
||||
@@ -268,7 +248,6 @@ def test_send_command_auto_saves_and_reuses_key_for_remote(monkeypatch, tmp_path
|
||||
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", tmp_path / "missing-registry.json")
|
||||
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CLI_TOKEN", raising=False)
|
||||
monkeypatch.delenv("BROWSER_CLI_KEY", raising=False)
|
||||
|
||||
from pathlib import Path as _Path
|
||||
@@ -281,7 +260,7 @@ def test_send_command_auto_saves_and_reuses_key_for_remote(monkeypatch, tmp_path
|
||||
monkeypatch.setattr("browser_cli.client._load_private_key", fake_load_private_key)
|
||||
monkeypatch.setattr(
|
||||
"browser_cli.client.remote_browser_targets",
|
||||
lambda endpoint, token=None, key=None: [BrowserTarget("default", "host:default", "", remote=endpoint)],
|
||||
lambda endpoint, key=None: [BrowserTarget("default", "host:default", "", remote=endpoint)],
|
||||
)
|
||||
|
||||
def fake_send_remote(endpoint, msg, private_key=None):
|
||||
|
||||
Reference in New Issue
Block a user