Files
CloakBrowser/tests/test_cli.py
T
CloakHQ 9021e0c07f feat(cli): info --proxy resolves exit IP + timezone + locale
Wire the existing launch()-time geoip resolver into the info/doctor command
across Python, JS, and .NET. With --proxy, info resolves the exit IP and the
timezone/locale a launch would apply (caching the GeoIP DB if absent) and prints
them in text and --json. Plain info is unchanged (no network) and now hints at
the flag. Adds diagnostics tests in all three suites.
2026-08-05 17:32:33 +02:00

355 lines
14 KiB
Python

"""Unit tests for the cloakbrowser CLI diagnostics (`info` / `doctor`)."""
import json
import os
import sys
from argparse import Namespace
from unittest.mock import patch
import pytest
import cloakbrowser.__main__
from cloakbrowser.__main__ import _binary_version, cmd_info
from cloakbrowser.license import LicenseInfo, ProReleaseInfo
def _run(args, *, key=None, license_info=None, sessions=None):
"""Run cmd_info with license resolution mocked and the real downloaders patched.
key=None -> no license -> free binary.
key set -> validate_license returns license_info (entitled to Pro if valid).
sessions -> what the seat-count lookup reports (it is mocked out, so a
non-quick Pro run never reaches the network).
Returns (download_free_mock, download_pro_mock, session_count_mock) so callers
can assert the command never triggers a binary download or an unwanted lookup.
"""
with (
patch("cloakbrowser.license.resolve_license_key", return_value=key),
patch("cloakbrowser.license.validate_license", return_value=license_info),
patch(
"cloakbrowser.license.get_active_session_count", return_value=sessions
) as mock_sessions,
patch("cloakbrowser.download._download_and_extract") as mock_dl_free,
patch("cloakbrowser.download._download_pro_binary") as mock_dl_pro,
):
cmd_info(args)
return mock_dl_free, mock_dl_pro, mock_sessions
def test_info_text_never_downloads(capsys):
free_dl, pro_dl, _ = _run(Namespace(quick=True, json=False))
free_dl.assert_not_called()
pro_dl.assert_not_called()
out = capsys.readouterr().out
assert "CloakBrowser diagnostics" in out
assert "Python:" in out
assert "Platform:" in out
assert "License: Free" in out
assert "Modules:" in out
def test_info_quick_skips_launch(capsys):
_run(Namespace(quick=True, json=False))
out = capsys.readouterr().out
assert "skipped (--quick)" in out
def test_info_without_proxy_shows_hint_and_never_resolves(capsys):
with patch("cloakbrowser.geoip.resolve_proxy_geo_with_ip") as resolver:
_run(Namespace(quick=True, json=False))
resolver.assert_not_called()
out = capsys.readouterr().out
assert "pass --proxy" in out
assert "Exit IP:" not in out
def test_info_proxy_resolves_and_prints_exit_ip(capsys):
with patch(
"cloakbrowser.geoip.resolve_proxy_geo_with_ip",
return_value=("Europe/Berlin", "de-DE", "203.0.113.9"),
) as resolver:
_run(Namespace(quick=True, json=False, proxy="http://p:8080"))
resolver.assert_called_once_with("http://p:8080")
out = capsys.readouterr().out
assert "Exit IP: 203.0.113.9" in out
assert "Timezone: Europe/Berlin" in out
assert "Locale: de-DE" in out
assert "pass --proxy" not in out # hint suppressed once resolved
def test_info_proxy_json_includes_resolved(capsys):
with patch(
"cloakbrowser.geoip.resolve_proxy_geo_with_ip",
return_value=("Europe/Berlin", "de-DE", "203.0.113.9"),
):
_run(Namespace(quick=True, json=True, proxy="http://p:8080"))
data = json.loads(capsys.readouterr().out)
assert data["geoip"]["resolved"] == {
"exit_ip": "203.0.113.9",
"timezone": "Europe/Berlin",
"locale": "de-DE",
}
def test_info_proxy_resolution_failure_is_reported_not_fatal(capsys):
with patch(
"cloakbrowser.geoip.resolve_proxy_geo_with_ip",
side_effect=RuntimeError("proxy refused"),
):
_run(Namespace(quick=True, json=False, proxy="http://p:8080"))
out = capsys.readouterr().out
assert "could not resolve" in out
assert "proxy refused" in out
def test_keyless_reports_free_binary(capsys):
"""No license key -> the binary section reflects the FREE binary."""
_run(Namespace(quick=True, json=True))
data = json.loads(capsys.readouterr().out)
assert data["binary"]["tier"] == "free"
assert data["license"]["tier"] == "free"
def test_valid_key_reports_pro_binary(capsys):
"""A server-validated key -> the binary section reflects the PRO binary.
``latest_version`` reports the server's latest (mocked); ``version`` is the
build that will actually launch (a cached Pro build if present, otherwise the
latest it will fetch). The two are surfaced separately so they can't diverge.
"""
valid = LicenseInfo(valid=True, plan="business", expires=None)
# quick=False: the server latest lookup is skipped under --quick (network-free),
# so exercise the full path to see latest_version populated.
release = ProReleaseInfo("148.0.0.0", "stable", "stable", False)
with patch("cloakbrowser.license.get_pro_latest_release", return_value=release):
_run(Namespace(quick=False, json=True), key="cb_test", license_info=valid)
data = json.loads(capsys.readouterr().out)
assert data["binary"]["tier"] == "pro"
assert data["binary"]["latest_version"] == "148.0.0.0"
# A Pro user always resolves to a Pro version (cached-or-latest), never the free base.
assert data["binary"]["version"]
assert data["license"]["tier"] == "business"
def test_quick_skips_pro_latest_lookup(capsys):
"""--quick keeps `info` network-free: no server latest-version lookup for Pro."""
valid = LicenseInfo(valid=True, plan="business", expires=None)
with patch(
"cloakbrowser.license.get_pro_latest_release",
return_value=ProReleaseInfo("148.0.0.0", "stable", "stable", False),
) as mock_latest:
_run(Namespace(quick=True, json=True), key="cb_test", license_info=valid)
data = json.loads(capsys.readouterr().out)
mock_latest.assert_not_called()
assert data["binary"]["latest_version"] is None
def test_info_reports_preview_stable_fallback(capsys):
valid = LicenseInfo(valid=True, plan="business", expires=None)
release = ProReleaseInfo("150.0.7871.114.3", "preview", "stable", True)
with (
patch.dict(os.environ, {"CLOAKBROWSER_RELEASE_CHANNEL": "preview"}),
patch("cloakbrowser.license.get_pro_latest_release", return_value=release),
):
_run(Namespace(quick=False, json=False), key="cb_test", license_info=valid)
assert "Channel: Preview → Stable fallback" in capsys.readouterr().out
def test_invalid_key_falls_back_to_free(capsys):
"""A key the server rejects -> not entitled -> free binary, not Pro."""
invalid = LicenseInfo(valid=False, plan="solo", expires=None)
_run(Namespace(quick=True, json=True), key="cb_bad", license_info=invalid)
data = json.loads(capsys.readouterr().out)
assert data["binary"]["tier"] == "free"
assert data["license"]["tier"] == "invalid"
# ── Seat count ────────────────────────────────────────
_PRO = LicenseInfo(valid=True, plan="business", expires=None)
def test_pro_reports_seats_in_use(capsys):
_run(Namespace(quick=False, json=True), key="cb_test", license_info=_PRO, sessions=3)
data = json.loads(capsys.readouterr().out)
assert data["license"]["sessions"] == {"active": 3}
def test_seat_line_printed_in_text_mode(capsys):
_run(Namespace(quick=False, json=False), key="cb_test", license_info=_PRO, sessions=3)
assert "Sessions: 3 seats in use" in capsys.readouterr().out
def test_seat_line_is_singular_for_one(capsys):
_run(Namespace(quick=False, json=False), key="cb_test", license_info=_PRO, sessions=1)
assert "Sessions: 1 seat in use" in capsys.readouterr().out
def test_zero_seats_reads_as_none_in_use_not_unavailable(capsys):
"""0 is a real answer ("nothing running"); only an unknown prints unavailable."""
_run(Namespace(quick=False, json=False), key="cb_test", license_info=_PRO, sessions=0)
out = capsys.readouterr().out
assert "Sessions: 0 seats in use" in out
assert "unavailable" not in out
def test_unknown_count_prints_unavailable(capsys):
"""Server unreachable, or the server itself reported the count as unknown
(leaseless mode) -> "unavailable", never a made-up number."""
_run(Namespace(quick=False, json=False), key="cb_test", license_info=_PRO, sessions=None)
assert "Sessions: unavailable" in capsys.readouterr().out
def test_quick_skips_the_seat_lookup(capsys):
"""--quick keeps `info` network-free — same rule as the Pro latest lookup."""
_, _, mock_sessions = _run(
Namespace(quick=True, json=True), key="cb_test", license_info=_PRO, sessions=3
)
data = json.loads(capsys.readouterr().out)
mock_sessions.assert_not_called()
assert "sessions" not in data["license"]
def test_free_tier_never_looks_up_seats(capsys):
"""A free tier holds no seats — don't ask the server about it."""
_, _, mock_sessions = _run(Namespace(quick=False, json=True))
data = json.loads(capsys.readouterr().out)
mock_sessions.assert_not_called()
assert "sessions" not in data["license"]
def test_invalid_key_never_looks_up_seats(capsys):
invalid = LicenseInfo(valid=False, plan="solo", expires=None)
_, _, mock_sessions = _run(
Namespace(quick=False, json=True), key="cb_bad", license_info=invalid
)
mock_sessions.assert_not_called()
def test_info_json_is_valid(capsys):
_run(Namespace(quick=True, json=True))
out = capsys.readouterr().out
data = json.loads(out)
assert data["environment"]["python"]
assert "modules" in data
# Upgrade hint is text-only; JSON exposes tier as a field instead.
assert "cloakbrowser.dev" not in out
def test_free_license_shows_upgrade_hint(capsys):
_run(Namespace(quick=True, json=False))
out = capsys.readouterr().out
# Keyless free: invite the free-latest login + point at paid for more sessions.
assert "License: Free (no key)" in out
assert "cloakbrowser login" in out
assert "For more than one concurrent session" in out
assert "cloakbrowser.dev" in out
# ---------------------------------------------------------------------------
# Launch test — exercises the real subprocess path (not --quick) against a stub
# executable, so the launch-test code is actually covered by CI.
# ---------------------------------------------------------------------------
pytestmark_posix = pytest.mark.skipif(
sys.platform == "win32", reason="uses a POSIX shell stub binary"
)
@pytestmark_posix
def test_binary_version_runs_stub(tmp_path):
stub = tmp_path / "fakechrome"
stub.write_text("#!/bin/sh\necho 'Chromium 1.2.3.4'\n")
stub.chmod(0o755)
ok, version, err = _binary_version(str(stub))
assert ok
assert "Chromium 1.2.3.4" in version
assert err == ""
def test_console_glyph_falls_back_on_legacy_windows_encoding(monkeypatch):
"""cmd.exe defaults to cp850/cp1252, which carry no check mark — printing one
there aborted the whole report with UnicodeEncodeError."""
import io
monkeypatch.setattr(
cloakbrowser.__main__.sys, "stdout", io.TextIOWrapper(io.BytesIO(), encoding="cp1252")
)
assert cloakbrowser.__main__._console_glyph("✓", "OK") == "OK"
assert cloakbrowser.__main__._console_glyph("→", "->") == "->"
# cp1252 does carry the em dash, so it is kept — the check is per glyph.
assert cloakbrowser.__main__._console_glyph("—", "-") == "—"
def test_console_glyph_kept_on_utf8(monkeypatch):
import io
monkeypatch.setattr(
cloakbrowser.__main__.sys, "stdout", io.TextIOWrapper(io.BytesIO(), encoding="utf-8")
)
assert cloakbrowser.__main__._console_glyph("✓", "OK") == "✓"
assert cloakbrowser.__main__._console_glyph("✗", "x") == "✗"
@pytestmark_posix
def test_binary_version_windows_probe_does_not_hang(tmp_path, monkeypatch):
"""Real Windows Chrome ignores --version and starts a browser instead of
printing, so the stub here hangs unless --no-startup-window is passed —
mirroring the binary rather than a stub that prints and exits."""
stub = tmp_path / "winchrome"
stub.write_text(
"#!/bin/sh\n"
'case "$*" in\n'
" *--no-startup-window*) exit 0 ;;\n"
" *) sleep 30 ;;\n" # stands in for the browser that never returns
"esac\n"
)
stub.chmod(0o755)
monkeypatch.setattr(cloakbrowser.__main__.platform, "system", lambda: "Windows")
ok, version, err = _binary_version(str(stub))
assert ok, f"probe hung on a healthy Windows binary: {err}"
assert version == "" # Windows prints no version
assert err == ""
@pytestmark_posix
def test_binary_version_reports_failure(tmp_path):
stub = tmp_path / "failchrome"
stub.write_text("#!/bin/sh\necho 'libfoo missing' >&2\nexit 1\n")
stub.chmod(0o755)
ok, version, err = _binary_version(str(stub))
assert not ok
assert "libfoo missing" in err
@pytestmark_posix
def test_launch_section_runs_binary_without_downloading(tmp_path, capsys):
"""Full non-quick run: the launch test executes the resolved binary, and no
download function is ever invoked."""
stub = tmp_path / "fakechrome"
stub.write_text("#!/bin/sh\necho 'Chromium 9.9.9.9'\n")
stub.chmod(0o755)
fake_binary = {
"version": "9.9.9.9",
"tier": "free",
"bundled_version": "x",
"path": str(stub),
"installed": True,
"cache_dir": str(tmp_path),
"override": None,
}
with (
patch("cloakbrowser.license.resolve_license_key", return_value=None),
patch("cloakbrowser.__main__._effective_binary", return_value=fake_binary),
patch("cloakbrowser.download._download_and_extract") as mock_dl_free,
patch("cloakbrowser.download._download_pro_binary") as mock_dl_pro,
):
cmd_info(Namespace(quick=False, json=True))
data = json.loads(capsys.readouterr().out)
assert data["launch"]["tested"] is True
assert data["launch"]["ok"] is True
assert "Chromium 9.9.9.9" in data["launch"]["version"]
mock_dl_free.assert_not_called()
mock_dl_pro.assert_not_called()