Files
browser-cli/tests/test_client.py
T
daniel156161 c1a5ef9dd7
Testing / test (push) Successful in 41s
Package Extension / package-extension (push) Successful in 35s
Build & Publish Package / publish (push) Successful in 46s
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>
2026-05-02 21:59:46 +02:00

281 lines
10 KiB
Python

import json
from pathlib import Path
import pytest
from browser_cli.client import (
BrowserNotConnected,
BrowserTarget,
_resolve_socket,
active_browser_targets,
display_browser_name,
key_for_remote,
send_command,
remote_target_for_alias,
)
from browser_cli.platform import endpoint_for_alias
def test_resolve_socket_raises_when_registry_missing(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json"))
with pytest.raises(BrowserNotConnected, match="Cannot resolve a browser socket automatically"):
_resolve_socket()
def test_resolve_socket_uses_only_active_registry_entry(monkeypatch, tmp_path):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
socket_path = tmp_path / "browser.sock"
socket_path.write_text("")
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"abc-uuid": str(socket_path)}))
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", registry_path)
assert _resolve_socket() == str(socket_path)
def test_resolve_socket_raises_when_multiple_active_entries(monkeypatch, tmp_path):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
first_socket = tmp_path / "one.sock"
second_socket = tmp_path / "two.sock"
first_socket.write_text("")
second_socket.write_text("")
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"uuid-1": str(first_socket), "uuid-2": str(second_socket)}))
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", registry_path)
with pytest.raises(BrowserNotConnected, match="Multiple browser instances are active: uuid-1, uuid-2"):
_resolve_socket()
def test_display_browser_name_uses_uuid_stem_for_default():
assert display_browser_name("default", "/tmp/.browser_cli/550e8400-e29b-41d4-a716-446655440000.sock") == (
"550e8400-e29b-41d4-a716-446655440000"
)
def test_resolve_socket_uses_platform_endpoint_for_explicit_alias(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", Path("/nonexistent/browser-cli-registry.json"))
assert _resolve_socket("work") == endpoint_for_alias("work")
def test_active_browser_targets_filters_stale_entries(monkeypatch, tmp_path):
active_socket = tmp_path / "work.sock"
active_socket.write_text("")
stale_socket = tmp_path / "stale.sock"
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"work": str(active_socket), "default": str(stale_socket)}))
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", registry_path)
targets = active_browser_targets(include_remotes=False)
assert len(targets) == 1
assert targets[0].profile == "work"
assert targets[0].display_name == "work"
def test_active_browser_targets_keeps_windows_registry_entries(monkeypatch, tmp_path):
registry_path = tmp_path / "registry.json"
registry_path.write_text(json.dumps({"work": r"\\.\pipe\browser-cli-work"}))
monkeypatch.setattr("browser_cli.client.REGISTRY_PATH", registry_path)
monkeypatch.setattr("browser_cli.client.is_windows", lambda: True)
targets = active_browser_targets(include_remotes=False)
assert len(targets) == 1
assert targets[0].socket_path == r"\\.\pipe\browser-cli-work"
def test_send_command_auto_routes_single_remote_target(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_PROFILE", raising=False)
monkeypatch.delenv("BROWSER_CLI_REMOTE", raising=False)
sent = {}
monkeypatch.setattr(
"browser_cli.client.remote_browser_targets",
lambda endpoint, key=None: [BrowserTarget("work", "host:work", "", remote=endpoint)],
)
def fake_send_remote(endpoint, msg, private_key=None):
sent.update(msg)
return json.dumps({"success": True, "data": "ok"}).encode("utf-8")
monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote)
assert send_command("tabs.list", remote="host:8765", key=None) == "ok"
assert sent["_route"] == "work"
assert "token" not in sent
def test_send_command_resolves_browser_alias_to_remote_target(monkeypatch):
monkeypatch.delenv("BROWSER_CLI_REMOTE", 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")],
)
def fake_send_remote(endpoint, msg, private_key=None):
sent["endpoint"] = endpoint
sent.update(msg)
return json.dumps({"success": True, "data": []}).encode("utf-8")
monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote)
assert send_command("tabs.list") == []
assert sent["endpoint"] == "host:8765"
assert sent["_route"] == "work"
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")],
)
target = remote_target_for_alias("host:8765:work")
assert target is not None
assert target.profile == "work"
assert target.remote == "host:8765"
def test_remote_target_for_alias_accepts_host_when_only_one_remote_target(monkeypatch):
remote_host = "browser-host.example"
remote_endpoint = f"{remote_host}:8765"
monkeypatch.setattr(
"browser_cli.client._remote_browser_targets",
lambda: [BrowserTarget("work", f"{remote_host}:work", "", remote=remote_endpoint)],
)
target = remote_target_for_alias(remote_host)
assert target is not None
assert target.profile == "work"
assert target.remote == remote_endpoint
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.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)],
)
def fake_send_remote(endpoint, msg, private_key=None):
sent["endpoint"] = endpoint
sent.update(msg)
return json.dumps({"success": True, "data": []}).encode("utf-8")
monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote)
assert send_command("tabs.list") == []
assert sent["endpoint"] == remote_endpoint
assert sent["_route"] == "work"
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"),
BrowserTarget("work", "host:work", "", remote="host:8765"),
],
)
assert remote_target_for_alias("host") is None
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, 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")
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: {}}), 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, key=None):
assert command == "browser-cli.targets"
assert remote == endpoint
return [{"profile": "work", "displayName": "work"}]
monkeypatch.setattr("browser_cli.client.send_command", fake_send_command)
targets = active_browser_targets()
assert len(targets) == 1
assert targets[0].profile == "work"
assert targets[0].display_name == "browser-host.example:work"
assert targets[0].remote == endpoint
def test_send_command_auto_saves_and_reuses_key_for_remote(monkeypatch, tmp_path):
"""--key agent is saved on first use; omitting --key on subsequent calls reuses it."""
import json as _json
remotes_path = tmp_path / "remotes.json"
remotes_path.write_text("{}", encoding="utf-8")
monkeypatch.setattr("browser_cli.client.REMOTE_REGISTRY_PATH", remotes_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_KEY", raising=False)
from pathlib import Path as _Path
used_keys = []
def fake_load_private_key(key_path=None):
used_keys.append(str(key_path) if key_path is not None else None)
return None # no actual key needed for this test
monkeypatch.setattr("browser_cli.client._load_private_key", fake_load_private_key)
monkeypatch.setattr(
"browser_cli.client.remote_browser_targets",
lambda endpoint, key=None: [BrowserTarget("default", "host:default", "", remote=endpoint)],
)
def fake_send_remote(endpoint, msg, private_key=None):
return _json.dumps({"success": True, "data": "ok"}).encode()
monkeypatch.setattr("browser_cli.client._send_remote", fake_send_remote)
# First call with explicit --key agent
send_command("tabs.list", remote="host:8765", key=_Path("agent"))
assert used_keys[-1] == "agent"
# Key must be persisted now
assert key_for_remote("host:8765") == "agent"
# Second call without --key — should reuse saved "agent"
send_command("tabs.list", remote="host:8765")
assert used_keys[-1] == "agent"