Files
browser-use/tests/ci/test_cli_cloud.py

282 lines
7.6 KiB
Python

"""Tests for browser-use cloud CLI command."""
import json
import subprocess
import sys
from pathlib import Path
from pytest_httpserver import HTTPServer
from werkzeug.wrappers import Request, Response
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def run_cli(*args: str, env_override: dict | None = None) -> subprocess.CompletedProcess:
"""Run the CLI as a subprocess, returning the result."""
import os
env = os.environ.copy()
# Prevent real API key from leaking into tests
env.pop('BROWSER_USE_API_KEY', None)
if env_override:
env.update(env_override)
return subprocess.run(
[sys.executable, '-m', 'browser_use.skill_cli.main', 'cloud', *args],
capture_output=True,
text=True,
env=env,
timeout=15,
)
# ---------------------------------------------------------------------------
# Login / Logout
# ---------------------------------------------------------------------------
def test_cloud_no_args_shows_usage():
result = run_cli()
# No args → usage on stdout, exit 1
assert result.returncode == 1
assert 'Usage' in result.stdout
assert 'login' in result.stdout
def test_cloud_login_saves_key(tmp_path: Path):
config_path = tmp_path / 'config.json'
result = run_cli(
'login',
'sk-test-key-123',
env_override={
'BROWSER_USE_HOME': str(tmp_path),
},
)
assert result.returncode == 0
assert 'saved' in result.stdout.lower()
# Verify file was written
real_config = tmp_path / 'config.json'
assert real_config.exists()
data = json.loads(real_config.read_text())
assert data['api_key'] == 'sk-test-key-123'
def test_cloud_logout_removes_key(tmp_path: Path):
# First save a key
config_file = tmp_path / 'config.json'
config_file.write_text(json.dumps({'api_key': 'sk-remove-me'}))
result = run_cli(
'logout',
env_override={'BROWSER_USE_HOME': str(tmp_path)},
)
assert result.returncode == 0
assert 'removed' in result.stdout.lower()
# Config file should be deleted (was only key)
assert not config_file.exists()
def test_cloud_logout_no_key(tmp_path: Path):
result = run_cli(
'logout',
env_override={'BROWSER_USE_HOME': str(tmp_path)},
)
assert result.returncode == 0
assert 'no api key' in result.stdout.lower()
# ---------------------------------------------------------------------------
# REST passthrough
# ---------------------------------------------------------------------------
def test_cloud_rest_get(httpserver: HTTPServer):
httpserver.expect_request('/api/v2/browsers', method='GET').respond_with_json(
{'browsers': [{'id': 'b1', 'status': 'running'}]}
)
result = run_cli(
'v2',
'GET',
'/browsers',
env_override={
'BROWSER_USE_API_KEY': 'sk-test',
'BROWSER_USE_CLOUD_BASE_URL_V2': httpserver.url_for('/api/v2'),
},
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert data['browsers'][0]['id'] == 'b1'
def test_cloud_rest_post_with_body(httpserver: HTTPServer):
body_to_send = {'task': 'Search for AI news', 'url': 'https://google.com'}
def handler(request: Request) -> Response:
assert request.content_type == 'application/json'
received = json.loads(request.data)
assert received == body_to_send
return Response(json.dumps({'id': 'task-1', 'status': 'created'}), content_type='application/json')
httpserver.expect_request('/api/v2/tasks', method='POST').respond_with_handler(handler)
result = run_cli(
'v2',
'POST',
'/tasks',
json.dumps(body_to_send),
env_override={
'BROWSER_USE_API_KEY': 'sk-test',
'BROWSER_USE_CLOUD_BASE_URL_V2': httpserver.url_for('/api/v2'),
},
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert data['id'] == 'task-1'
def test_cloud_rest_sends_auth_header(httpserver: HTTPServer):
def handler(request: Request) -> Response:
assert request.headers.get('X-Browser-Use-API-Key') == 'sk-secret-key'
return Response(json.dumps({'ok': True}), content_type='application/json')
httpserver.expect_request('/api/v2/test', method='GET').respond_with_handler(handler)
result = run_cli(
'v2',
'GET',
'/test',
env_override={
'BROWSER_USE_API_KEY': 'sk-secret-key',
'BROWSER_USE_CLOUD_BASE_URL_V2': httpserver.url_for('/api/v2'),
},
)
assert result.returncode == 0
def test_cloud_rest_4xx_exits_2(httpserver: HTTPServer):
httpserver.expect_request('/api/v2/bad', method='GET').respond_with_json({'error': 'not found'}, status=404)
result = run_cli(
'v2',
'GET',
'/bad',
env_override={
'BROWSER_USE_API_KEY': 'sk-test',
'BROWSER_USE_CLOUD_BASE_URL_V2': httpserver.url_for('/api/v2'),
# Prevent spec fetch from hanging
'BROWSER_USE_OPENAPI_SPEC_URL_V2': 'http://127.0.0.1:1/nope',
},
)
assert result.returncode == 2
assert 'HTTP 404' in result.stderr
def test_cloud_rest_no_api_key_errors(tmp_path: Path):
result = run_cli(
'v2',
'GET',
'/browsers',
env_override={
'BROWSER_USE_HOME': str(tmp_path),
},
)
# _get_api_key calls sys.exit(1)
assert result.returncode == 1
assert 'no api key' in result.stderr.lower()
# ---------------------------------------------------------------------------
# Polling
# ---------------------------------------------------------------------------
def test_cloud_poll_finishes(httpserver: HTTPServer):
# First call: running, second call: finished
call_count = {'n': 0}
def handler(request: Request) -> Response:
call_count['n'] += 1
if call_count['n'] == 1:
return Response(json.dumps({'status': 'running', 'cost': 0.0012}), content_type='application/json')
return Response(json.dumps({'status': 'finished', 'cost': 0.0050, 'result': 'done'}), content_type='application/json')
httpserver.expect_request('/api/v2/tasks/t-123', method='GET').respond_with_handler(handler)
result = run_cli(
'v2',
'poll',
't-123',
env_override={
'BROWSER_USE_API_KEY': 'sk-test',
'BROWSER_USE_CLOUD_BASE_URL_V2': httpserver.url_for('/api/v2'),
},
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert data['status'] == 'finished'
assert 'status: finished' in result.stderr
def test_cloud_poll_failed_exits_2(httpserver: HTTPServer):
httpserver.expect_request('/api/v2/tasks/t-fail', method='GET').respond_with_json(
{'status': 'failed', 'cost': 0.0001, 'error': 'timeout'}
)
result = run_cli(
'v2',
'poll',
't-fail',
env_override={
'BROWSER_USE_API_KEY': 'sk-test',
'BROWSER_USE_CLOUD_BASE_URL_V2': httpserver.url_for('/api/v2'),
},
)
assert result.returncode == 2
# ---------------------------------------------------------------------------
# URL construction
# ---------------------------------------------------------------------------
def test_cloud_url_construction(httpserver: HTTPServer):
"""Path without leading / should still work."""
httpserver.expect_request('/api/v2/browsers', method='GET').respond_with_json({'ok': True})
result = run_cli(
'v2',
'GET',
'browsers', # no leading /
env_override={
'BROWSER_USE_API_KEY': 'sk-test',
'BROWSER_USE_CLOUD_BASE_URL_V2': httpserver.url_for('/api/v2'),
},
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert data['ok'] is True
# ---------------------------------------------------------------------------
# Help
# ---------------------------------------------------------------------------
def test_cloud_help_flag():
"""--help should show something useful even without spec."""
result = run_cli(
'v2',
'--help',
env_override={
# Point to unreachable spec URL so static fallback is used
'BROWSER_USE_OPENAPI_SPEC_URL_V2': 'http://127.0.0.1:1/nope',
},
)
assert result.returncode == 0
assert 'browser-use cloud v2' in result.stdout.lower()