test: add comprehensive unit tests for all public APIs

Python (75 new tests):
- launch_context(): viewport, timezone bypass, geoip, close cleanup, error cleanup
- launch_persistent_context(): sync + async, args, proxy, close/pw.stop()
- config: binary paths, archive names, cache dir, stealth args profiles
- extract: tar/zip with path traversal protection, .app bundle preservation
- ensure_binary(), clear_cache(), check_for_update(), version markers
- geoip: private IP detection

JavaScript (26 new tests):
- puppeteer wrapper: stealth args, proxy string/dict, auth monkey-patch
- launchContext/launchPersistentContext: viewport, timezone, proxy, close
- ensureBinary, clearCache, checkForUpdate, archive helpers

Total: 169 Python + 88 JS tests (was 59 + 47)
This commit is contained in:
Cloak-HQ
2026-03-05 03:02:46 +01:00
parent 05fa1a052a
commit 0719f750ef
10 changed files with 1311 additions and 2 deletions
+24
View File
@@ -1,11 +1,13 @@
import { describe, it, expect } from "vitest";
import {
CHROMIUM_VERSION,
getArchiveExt,
getChromiumVersion,
getDefaultStealthArgs,
getCacheDir,
getBinaryDir,
getDownloadUrl,
getFallbackDownloadUrl,
} from "../src/config.js";
import { _buildArgsForTest, migrateTimezoneId } from "../src/playwright.js";
@@ -68,6 +70,28 @@ describe("config", () => {
});
});
describe("archive helpers", () => {
it("getArchiveExt returns correct extension for platform", () => {
const ext = getArchiveExt();
if (process.platform === "win32") {
expect(ext).toBe(".zip");
} else {
expect(ext).toBe(".tar.gz");
}
});
it("getFallbackDownloadUrl uses GitHub Releases", () => {
const url = getFallbackDownloadUrl("145.0.0.0");
expect(url).toContain("github.com/CloakHQ/cloakbrowser/releases/download");
expect(url).toContain("chromium-v145.0.0.0");
});
it("getFallbackDownloadUrl uses default version", () => {
const url = getFallbackDownloadUrl();
expect(url).toContain(`chromium-v${getChromiumVersion()}`);
});
});
describe("buildArgs timezone/locale", () => {
it("injects --fingerprint-timezone when timezone is set", () => {
const args = _buildArgsForTest({ timezone: "America/New_York" });
+172 -2
View File
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
import { binaryInfo } from "../src/download.js";
import { getChromiumVersion } from "../src/config.js";
import { DEFAULT_VIEWPORT, getChromiumVersion } from "../src/config.js";
describe("binaryInfo", () => {
it("returns correct structure", () => {
@@ -37,3 +37,173 @@ describe.skipIf(!process.env.CLOAKBROWSER_BINARY_PATH)(
}, 30_000);
}
);
// ---------------------------------------------------------------------------
// launchContext / launchPersistentContext unit tests (mock playwright-core)
// ---------------------------------------------------------------------------
describe("launchContext (unit)", () => {
let mockContext: any;
let mockBrowser: any;
let mockChromium: any;
const origEnv = process.env.CLOAKBROWSER_BINARY_PATH;
beforeEach(() => {
process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
const origClose = vi.fn();
mockContext = { close: origClose, _origClose: origClose };
mockBrowser = {
newContext: vi.fn().mockResolvedValue(mockContext),
close: vi.fn(),
};
mockChromium = { launch: vi.fn().mockResolvedValue(mockBrowser) };
vi.doMock("playwright-core", () => ({ chromium: mockChromium }));
});
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
if (origEnv) {
process.env.CLOAKBROWSER_BINARY_PATH = origEnv;
} else {
delete process.env.CLOAKBROWSER_BINARY_PATH;
}
});
it("applies DEFAULT_VIEWPORT when no viewport given", async () => {
const { launchContext } = await import("../src/playwright.js");
await launchContext();
const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.viewport).toEqual(DEFAULT_VIEWPORT);
});
it("uses custom viewport when provided", async () => {
const { launchContext } = await import("../src/playwright.js");
const custom = { width: 1280, height: 720 };
await launchContext({ viewport: custom });
const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.viewport).toEqual(custom);
});
it("forwards userAgent to newContext", async () => {
const { launchContext } = await import("../src/playwright.js");
await launchContext({ userAgent: "Custom/1.0" });
const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.userAgent).toBe("Custom/1.0");
});
it("passes timezone to context timezoneId, not to launch", async () => {
const { launchContext } = await import("../src/playwright.js");
await launchContext({ timezone: "America/New_York" });
// launch() called with timezone: undefined (skipped for binary flag)
const launchArgs = mockChromium.launch.mock.calls[0][0];
const hasTimezoneFlag = launchArgs.args.some((a: string) =>
a.startsWith("--fingerprint-timezone=")
);
expect(hasTimezoneFlag).toBe(false);
// newContext() gets timezoneId
const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.timezoneId).toBe("America/New_York");
});
it("forwards colorScheme to newContext", async () => {
const { launchContext } = await import("../src/playwright.js");
await launchContext({ colorScheme: "dark" });
const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.colorScheme).toBe("dark");
});
it("close() also closes browser", async () => {
const { launchContext } = await import("../src/playwright.js");
const ctx = await launchContext();
await ctx.close();
// Original context close called
expect(mockContext._origClose).toHaveBeenCalledOnce();
// Browser also closed
expect(mockBrowser.close).toHaveBeenCalledOnce();
});
});
describe("launchPersistentContext (unit)", () => {
let mockContext: any;
let mockChromium: any;
const origEnv = process.env.CLOAKBROWSER_BINARY_PATH;
beforeEach(() => {
process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
mockContext = { close: vi.fn(), pages: vi.fn().mockReturnValue([]) };
mockChromium = {
launchPersistentContext: vi.fn().mockResolvedValue(mockContext),
};
vi.doMock("playwright-core", () => ({ chromium: mockChromium }));
});
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
if (origEnv) {
process.env.CLOAKBROWSER_BINARY_PATH = origEnv;
} else {
delete process.env.CLOAKBROWSER_BINARY_PATH;
}
});
it("applies DEFAULT_VIEWPORT", async () => {
const { launchPersistentContext } = await import("../src/playwright.js");
await launchPersistentContext({ userDataDir: "/tmp/profile" });
const args = mockChromium.launchPersistentContext.mock.calls[0][1];
expect(args.viewport).toEqual(DEFAULT_VIEWPORT);
});
it("passes timezone and locale to context", async () => {
const { launchPersistentContext } = await import("../src/playwright.js");
await launchPersistentContext({
userDataDir: "/tmp/profile",
timezone: "Asia/Tokyo",
locale: "ja-JP",
});
const args = mockChromium.launchPersistentContext.mock.calls[0][1];
expect(args.timezoneId).toBe("Asia/Tokyo");
expect(args.locale).toBe("ja-JP");
// Also in binary args
expect(args.args).toContain("--fingerprint-timezone=Asia/Tokyo");
expect(args.args).toContain("--lang=ja-JP");
});
it("forwards proxy string", async () => {
const { launchPersistentContext } = await import("../src/playwright.js");
await launchPersistentContext({
userDataDir: "/tmp/profile",
proxy: "http://user:pass@proxy:8080",
});
const args = mockChromium.launchPersistentContext.mock.calls[0][1];
expect(args.proxy.server).toBe("http://proxy:8080");
expect(args.proxy.username).toBe("user");
expect(args.proxy.password).toBe("pass");
});
it("forwards userAgent and colorScheme", async () => {
const { launchPersistentContext } = await import("../src/playwright.js");
await launchPersistentContext({
userDataDir: "/tmp/profile",
userAgent: "Custom/1.0",
colorScheme: "dark",
});
const args = mockChromium.launchPersistentContext.mock.calls[0][1];
expect(args.userAgent).toBe("Custom/1.0");
expect(args.colorScheme).toBe("dark");
});
});
+113
View File
@@ -0,0 +1,113 @@
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
// Mock puppeteer-core and download before importing the module under test
vi.mock("puppeteer-core", () => ({
default: {
launch: vi.fn(),
},
}));
vi.mock("../src/download.js", () => ({
ensureBinary: vi.fn().mockResolvedValue("/fake/chrome"),
}));
vi.mock("../src/geoip.js", () => ({
resolveProxyGeo: vi.fn().mockResolvedValue({ timezone: null, locale: null }),
}));
describe("puppeteer launch", () => {
let puppeteerMock: any;
let mockBrowser: any;
beforeEach(async () => {
puppeteerMock = await import("puppeteer-core");
mockBrowser = {
newPage: vi.fn().mockResolvedValue({
authenticate: vi.fn(),
}),
close: vi.fn(),
};
vi.mocked(puppeteerMock.default.launch).mockResolvedValue(mockBrowser);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("calls ensureBinary and launches with binary path", async () => {
const { launch } = await import("../src/puppeteer.js");
await launch();
expect(puppeteerMock.default.launch).toHaveBeenCalledWith(
expect.objectContaining({
executablePath: "/fake/chrome",
})
);
});
it("includes stealth args by default", async () => {
const { launch } = await import("../src/puppeteer.js");
await launch();
const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0];
expect(callArgs.args.some((a: string) => a.startsWith("--fingerprint="))).toBe(true);
expect(callArgs.args).toContain("--no-sandbox");
});
it("excludes stealth args when stealthArgs=false", async () => {
const { launch } = await import("../src/puppeteer.js");
await launch({ stealthArgs: false });
const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0];
expect(callArgs.args.some((a: string) => a.startsWith("--fingerprint="))).toBe(false);
});
it("adds --proxy-server for string proxy", async () => {
const { launch } = await import("../src/puppeteer.js");
await launch({ proxy: "http://proxy:8080" });
const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0];
expect(callArgs.args).toContain("--proxy-server=http://proxy:8080");
});
it("adds --proxy-bypass-list for dict proxy with bypass", async () => {
const { launch } = await import("../src/puppeteer.js");
await launch({
proxy: { server: "http://proxy:8080", bypass: ".google.com,localhost" },
});
const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0];
expect(callArgs.args).toContain("--proxy-server=http://proxy:8080");
expect(callArgs.args).toContain("--proxy-bypass-list=.google.com,localhost");
});
it("monkey-patches newPage for proxy auth", async () => {
const { launch } = await import("../src/puppeteer.js");
const browser = await launch({ proxy: "http://user:pass@proxy:8080" });
// newPage should auto-authenticate
const page = await browser.newPage();
expect(page.authenticate).toHaveBeenCalledWith({
username: "user",
password: "pass",
});
});
it("injects timezone and locale as binary flags", async () => {
const { launch } = await import("../src/puppeteer.js");
await launch({ timezone: "Asia/Tokyo", locale: "ja-JP" });
const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0];
expect(callArgs.args).toContain("--fingerprint-timezone=Asia/Tokyo");
expect(callArgs.args).toContain("--lang=ja-JP");
});
it("merges extra args", async () => {
const { launch } = await import("../src/puppeteer.js");
await launch({ args: ["--disable-gpu", "--no-first-run"] });
const callArgs = vi.mocked(puppeteerMock.default.launch).mock.calls[0][0];
expect(callArgs.args).toContain("--disable-gpu");
expect(callArgs.args).toContain("--no-first-run");
});
});
+54
View File
@@ -9,7 +9,11 @@ import {
versionNewer,
} from "../src/config.js";
import {
binaryInfo,
checkForUpdate,
checkWrapperUpdate,
clearCache,
ensureBinary,
getLatestChromiumVersion,
parseChecksums,
resetWrapperUpdateChecked,
@@ -271,3 +275,53 @@ describe("effective version", () => {
expect(getEffectiveVersion()).toBe(getChromiumVersion());
});
});
describe("ensureBinary", () => {
afterEach(() => {
delete process.env.CLOAKBROWSER_BINARY_PATH;
});
it("returns local override when set", async () => {
// Use this test file as a "binary" that exists
process.env.CLOAKBROWSER_BINARY_PATH = __filename;
const result = await ensureBinary();
expect(result).toBe(__filename);
});
it("throws when local override path missing", async () => {
process.env.CLOAKBROWSER_BINARY_PATH = "/nonexistent/chrome";
await expect(ensureBinary()).rejects.toThrow("does not exist");
});
});
describe("clearCache", () => {
it("does not throw when cache dir missing", () => {
const orig = process.env.CLOAKBROWSER_CACHE_DIR;
process.env.CLOAKBROWSER_CACHE_DIR = "/tmp/cloakbrowser-test-nonexistent";
expect(() => clearCache()).not.toThrow();
if (orig) {
process.env.CLOAKBROWSER_CACHE_DIR = orig;
} else {
delete process.env.CLOAKBROWSER_CACHE_DIR;
}
});
});
describe("checkForUpdate", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("returns null when no newer version", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: true,
json: async () => [],
} as Response);
expect(await checkForUpdate()).toBeNull();
});
it("returns null on network error", async () => {
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("timeout"));
expect(await checkForUpdate()).toBeNull();
});
});
+142
View File
@@ -0,0 +1,142 @@
"""Unit tests for config.py — platform detection, paths, stealth args."""
import os
from unittest.mock import patch
import pytest
from cloakbrowser.config import (
get_archive_ext,
get_archive_name,
get_binary_path,
get_cache_dir,
get_chromium_version,
get_default_stealth_args,
get_fallback_download_url,
get_platform_tag,
)
# ---------------------------------------------------------------------------
# Platform-specific binary paths
# ---------------------------------------------------------------------------
class TestGetBinaryPath:
def test_linux(self):
with patch("cloakbrowser.config.platform.system", return_value="Linux"):
path = get_binary_path("145.0.0.0")
assert str(path).endswith("chromium-145.0.0.0/chrome")
def test_darwin(self):
with patch("cloakbrowser.config.platform.system", return_value="Darwin"):
path = get_binary_path("145.0.0.0")
assert str(path).endswith("chromium-145.0.0.0/Chromium.app/Contents/MacOS/Chromium")
def test_windows(self):
with patch("cloakbrowser.config.platform.system", return_value="Windows"):
path = get_binary_path("145.0.0.0")
assert str(path).endswith("chromium-145.0.0.0/chrome.exe")
# ---------------------------------------------------------------------------
# Archive extension and name
# ---------------------------------------------------------------------------
class TestArchive:
def test_ext_windows(self):
with patch("cloakbrowser.config.platform.system", return_value="Windows"):
assert get_archive_ext() == ".zip"
def test_ext_unix(self):
for system in ("Linux", "Darwin"):
with patch("cloakbrowser.config.platform.system", return_value=system):
assert get_archive_ext() == ".tar.gz"
def test_archive_name(self):
tag = get_platform_tag()
ext = get_archive_ext()
assert get_archive_name() == f"cloakbrowser-{tag}{ext}"
def test_archive_name_custom_tag(self):
name = get_archive_name("linux-x64")
assert "cloakbrowser-linux-x64" in name
# ---------------------------------------------------------------------------
# Download URLs
# ---------------------------------------------------------------------------
class TestFallbackUrl:
def test_github_releases_format(self):
url = get_fallback_download_url("145.0.0.0")
assert "github.com/CloakHQ/cloakbrowser/releases/download" in url
assert "chromium-v145.0.0.0" in url
def test_default_version(self):
url = get_fallback_download_url()
version = get_chromium_version()
assert f"chromium-v{version}" in url
# ---------------------------------------------------------------------------
# Cache directory
# ---------------------------------------------------------------------------
class TestCacheDir:
def test_default_path(self):
with patch.dict(os.environ, {}, clear=False):
# Remove override if set
env = os.environ.copy()
env.pop("CLOAKBROWSER_CACHE_DIR", None)
with patch.dict(os.environ, env, clear=True):
path = get_cache_dir()
assert str(path).endswith(".cloakbrowser")
def test_env_override(self, tmp_path):
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
assert get_cache_dir() == tmp_path
# ---------------------------------------------------------------------------
# Platform tag
# ---------------------------------------------------------------------------
class TestPlatformTag:
def test_unsupported_raises(self):
with patch("cloakbrowser.config.platform.system", return_value="FreeBSD"):
with patch("cloakbrowser.config.platform.machine", return_value="x86_64"):
with pytest.raises(RuntimeError, match="Unsupported platform"):
get_platform_tag()
# ---------------------------------------------------------------------------
# Stealth args
# ---------------------------------------------------------------------------
class TestStealthArgs:
def test_seed_uniqueness(self):
"""Two calls should produce different fingerprint seeds."""
args1 = get_default_stealth_args()
args2 = get_default_stealth_args()
seed1 = [a for a in args1 if a.startswith("--fingerprint=")][0]
seed2 = [a for a in args2 if a.startswith("--fingerprint=")][0]
# Seeds are random 10000-99999 — extremely unlikely to collide
assert seed1 != seed2
def test_macos_profile(self):
with patch("cloakbrowser.config.platform.system", return_value="Darwin"):
args = get_default_stealth_args()
assert "--fingerprint-platform=macos" in args
assert any("Apple" in a for a in args)
def test_linux_windows_profile(self):
with patch("cloakbrowser.config.platform.system", return_value="Linux"):
args = get_default_stealth_args()
assert "--fingerprint-platform=windows" in args
assert any("NVIDIA" in a for a in args)
+192
View File
@@ -0,0 +1,192 @@
"""Unit tests for archive extraction — path traversal protection, flattening, permissions."""
import io
import os
import platform
import stat
import tarfile
import zipfile
import pytest
from cloakbrowser.download import (
_extract_tar,
_extract_zip,
_flatten_single_subdir,
_is_executable,
_make_executable,
)
# ---------------------------------------------------------------------------
# tar.gz extraction
# ---------------------------------------------------------------------------
def _create_tar_gz(tmp_path, members: dict[str, bytes]) -> "Path":
"""Create a tar.gz with given {name: content} members."""
archive = tmp_path / "test.tar.gz"
with tarfile.open(archive, "w:gz") as tar:
for name, content in members.items():
info = tarfile.TarInfo(name=name)
info.size = len(content)
tar.addfile(info, io.BytesIO(content))
return archive
class TestExtractTar:
def test_basic(self, tmp_path):
archive = _create_tar_gz(tmp_path, {"chrome": b"binary", "lib/libfoo.so": b"lib"})
dest = tmp_path / "out"
dest.mkdir()
_extract_tar(archive, dest)
assert (dest / "chrome").read_bytes() == b"binary"
assert (dest / "lib" / "libfoo.so").read_bytes() == b"lib"
def test_path_traversal_blocked(self, tmp_path):
archive = tmp_path / "evil.tar.gz"
with tarfile.open(archive, "w:gz") as tar:
info = tarfile.TarInfo(name="../../../etc/passwd")
info.size = 4
tar.addfile(info, io.BytesIO(b"evil"))
dest = tmp_path / "out"
dest.mkdir()
with pytest.raises(RuntimeError, match="path traversal"):
_extract_tar(archive, dest)
def test_suspicious_symlink_skipped(self, tmp_path):
"""Symlinks with absolute targets are skipped (logged as warning)."""
archive = tmp_path / "symlink.tar.gz"
with tarfile.open(archive, "w:gz") as tar:
# Normal file
info = tarfile.TarInfo(name="chrome")
info.size = 6
tar.addfile(info, io.BytesIO(b"binary"))
# Suspicious symlink
sym = tarfile.TarInfo(name="evil_link")
sym.type = tarfile.SYMTYPE
sym.linkname = "/etc/passwd"
tar.addfile(sym)
dest = tmp_path / "out"
dest.mkdir()
_extract_tar(archive, dest)
# Normal file extracted
assert (dest / "chrome").exists()
# Suspicious symlink was skipped
assert not (dest / "evil_link").exists()
# ---------------------------------------------------------------------------
# zip extraction
# ---------------------------------------------------------------------------
def _create_zip(tmp_path, members: dict[str, bytes]) -> "Path":
"""Create a zip with given {name: content} members."""
archive = tmp_path / "test.zip"
with zipfile.ZipFile(archive, "w") as zf:
for name, content in members.items():
zf.writestr(name, content)
return archive
class TestExtractZip:
def test_basic(self, tmp_path):
archive = _create_zip(tmp_path, {"chrome.exe": b"binary", "lib/foo.dll": b"lib"})
dest = tmp_path / "out"
dest.mkdir()
_extract_zip(archive, dest)
assert (dest / "chrome.exe").read_bytes() == b"binary"
assert (dest / "lib" / "foo.dll").read_bytes() == b"lib"
def test_path_traversal_blocked(self, tmp_path):
archive = tmp_path / "evil.zip"
with zipfile.ZipFile(archive, "w") as zf:
zf.writestr("../../../etc/passwd", "evil")
dest = tmp_path / "out"
dest.mkdir()
with pytest.raises(RuntimeError, match="path traversal"):
_extract_zip(archive, dest)
# ---------------------------------------------------------------------------
# Directory flattening
# ---------------------------------------------------------------------------
class TestFlatten:
def test_single_subdir_flattened(self, tmp_path):
"""Single subdir contents moved up."""
dest = tmp_path / "out"
dest.mkdir()
subdir = dest / "fingerprint-chromium-custom-v14"
subdir.mkdir()
(subdir / "chrome").write_bytes(b"binary")
(subdir / "lib").mkdir()
_flatten_single_subdir(dest)
assert (dest / "chrome").read_bytes() == b"binary"
assert (dest / "lib").is_dir()
assert not subdir.exists()
def test_app_bundle_preserved(self, tmp_path):
""".app directory NOT flattened (macOS bundle)."""
dest = tmp_path / "out"
dest.mkdir()
app = dest / "Chromium.app"
app.mkdir()
(app / "Contents").mkdir()
(app / "Contents" / "MacOS").mkdir()
(app / "Contents" / "MacOS" / "Chromium").write_bytes(b"binary")
_flatten_single_subdir(dest)
# .app bundle kept intact
assert app.is_dir()
assert (app / "Contents" / "MacOS" / "Chromium").exists()
def test_noop_multiple_entries(self, tmp_path):
"""Multiple entries at top level — no flattening."""
dest = tmp_path / "out"
dest.mkdir()
(dest / "chrome").write_bytes(b"binary")
(dest / "lib").mkdir()
_flatten_single_subdir(dest)
# Nothing moved
assert (dest / "chrome").exists()
assert (dest / "lib").is_dir()
# ---------------------------------------------------------------------------
# Permissions
# ---------------------------------------------------------------------------
class TestPermissions:
@pytest.mark.skipif(platform.system() == "Windows", reason="chmod not applicable on Windows")
def test_make_executable(self, tmp_path):
binary = tmp_path / "chrome"
binary.write_bytes(b"binary")
binary.chmod(0o644)
assert not _is_executable(binary)
_make_executable(binary)
assert _is_executable(binary)
def test_is_executable_true(self, tmp_path):
binary = tmp_path / "chrome"
binary.write_bytes(b"binary")
binary.chmod(0o755)
assert _is_executable(binary)
def test_is_executable_false(self, tmp_path):
binary = tmp_path / "chrome"
binary.write_bytes(b"binary")
binary.chmod(0o644)
assert not _is_executable(binary)
+21
View File
@@ -7,6 +7,7 @@ import pytest
from cloakbrowser.browser import _maybe_resolve_geoip
from cloakbrowser.geoip import (
COUNTRY_LOCALE_MAP,
_is_private_ip,
_resolve_proxy_ip,
)
@@ -136,3 +137,23 @@ def test_maybe_resolve_fills_both():
tz, loc = _maybe_resolve_geoip(True, "http://proxy:8080", None, None)
assert tz == "Europe/Berlin"
assert loc == "de-DE"
# ---------------------------------------------------------------------------
# _is_private_ip
# ---------------------------------------------------------------------------
def test_private_ip_loopback():
assert _is_private_ip("127.0.0.1") is True
def test_private_ip_rfc1918():
assert _is_private_ip("192.168.1.1") is True
assert _is_private_ip("10.0.0.1") is True
assert _is_private_ip("172.16.0.1") is True
def test_private_ip_public():
assert _is_private_ip("8.8.8.8") is False
assert _is_private_ip("64.176.168.43") is False
+213
View File
@@ -0,0 +1,213 @@
"""Unit tests for launch_context() — context kwargs, viewport defaults, close cleanup."""
import warnings
from unittest.mock import MagicMock, call, patch
import pytest
from cloakbrowser.config import DEFAULT_VIEWPORT
# All tests mock launch() to avoid needing a binary.
# launch_context() calls launch() internally, then browser.new_context().
def _make_mock_browser():
"""Create a mock browser with new_context() returning a mock context."""
browser = MagicMock()
context = MagicMock()
browser.new_context.return_value = context
return browser, context
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_default_viewport(mock_launch, _mock_bin):
"""DEFAULT_VIEWPORT applied when no viewport given."""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
launch_context()
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["viewport"] == DEFAULT_VIEWPORT
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_custom_viewport(mock_launch, _mock_bin):
"""Custom viewport overrides DEFAULT_VIEWPORT."""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
custom = {"width": 1280, "height": 720}
launch_context(viewport=custom)
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["viewport"] == custom
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_user_agent(mock_launch, _mock_bin):
"""user_agent forwarded to new_context()."""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
launch_context(user_agent="Mozilla/5.0 Custom")
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["user_agent"] == "Mozilla/5.0 Custom"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_locale_forwarded(mock_launch, _mock_bin):
"""locale flows to both launch() binary args AND new_context()."""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
launch_context(locale="de-DE")
# Locale in launch() call (for --lang binary flag)
assert mock_launch.call_args[1]["locale"] == "de-DE"
# Locale in new_context() call
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["locale"] == "de-DE"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_timezone_via_context_not_binary(mock_launch, _mock_bin):
"""timezone passed to new_context(timezone_id=...) but NOT to launch(timezone=...).
This is intentional: the --fingerprint-timezone binary flag only applies to the
default context and would conflict with Playwright's timezone_id on new contexts.
"""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
launch_context(timezone="America/New_York")
# timezone=None in launch() — binary flag skipped
assert mock_launch.call_args[1]["timezone"] is None
# timezone_id in new_context()
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["timezone_id"] == "America/New_York"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_color_scheme(mock_launch, _mock_bin):
"""color_scheme forwarded to new_context()."""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
launch_context(color_scheme="dark")
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["color_scheme"] == "dark"
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=("Europe/Berlin", "de-DE"))
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_geoip_resolution(mock_launch, _mock_bin, _mock_geoip):
"""geoip fills timezone+locale, both flow to correct places."""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
launch_context(proxy="http://proxy:8080", geoip=True)
# Locale goes to launch() for binary flag
assert mock_launch.call_args[1]["locale"] == "de-DE"
# Timezone goes to context, not binary
assert mock_launch.call_args[1]["timezone"] is None
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["timezone_id"] == "Europe/Berlin"
assert ctx_kwargs[1]["locale"] == "de-DE"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_timezone_id_deprecation(mock_launch, _mock_bin):
"""timezone_id kwarg triggers FutureWarning, value migrated to timezone."""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
launch_context(timezone_id="Europe/Paris")
assert len(w) == 1
assert issubclass(w[0].category, FutureWarning)
assert "timezone_id" in str(w[0].message)
# Migrated value flows to context
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["timezone_id"] == "Europe/Paris"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_close_closes_browser(mock_launch, _mock_bin):
"""context.close() also calls browser.close()."""
browser, context = _make_mock_browser()
# Save reference before launch_context() monkey-patches context.close
original_ctx_close = context.close
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
ctx = launch_context()
# The returned context has a patched close()
ctx.close()
# Original context close was called
original_ctx_close.assert_called_once()
# Browser close was also called
browser.close.assert_called_once()
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_error_closes_browser(mock_launch, _mock_bin):
"""If new_context() raises, browser is still closed."""
browser = MagicMock()
browser.new_context.side_effect = RuntimeError("context creation failed")
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
with pytest.raises(RuntimeError, match="context creation failed"):
launch_context()
browser.close.assert_called_once()
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser.launch")
def test_kwargs_passthrough(mock_launch, _mock_bin):
"""Extra kwargs forwarded to new_context(), NOT to launch().
Important contract: kwargs like record_video_dir go to context creation,
not browser launch.
"""
browser, context = _make_mock_browser()
mock_launch.return_value = browser
from cloakbrowser.browser import launch_context
launch_context(record_video_dir="/tmp/videos")
# Verify kwarg reached new_context()
ctx_kwargs = browser.new_context.call_args
assert ctx_kwargs[1]["record_video_dir"] == "/tmp/videos"
# Verify kwarg did NOT leak to launch()
launch_kwargs = mock_launch.call_args[1]
assert "record_video_dir" not in launch_kwargs
+262
View File
@@ -0,0 +1,262 @@
"""Unit tests for launch_persistent_context() and launch_persistent_context_async().
All tests mock patchright to avoid needing a binary.
"""
import warnings
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from cloakbrowser.config import DEFAULT_VIEWPORT
def _make_mock_pw_and_context():
"""Create mock sync_playwright chain returning a mock context."""
context = MagicMock()
pw = MagicMock()
pw.chromium.launch_persistent_context.return_value = context
pw_cm = MagicMock()
pw_cm.start.return_value = pw
return pw_cm, pw, context
# ---------------------------------------------------------------------------
# Sync: launch_persistent_context()
# ---------------------------------------------------------------------------
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
def test_persistent_context_args_built(_mock_geoip, _mock_bin):
"""Stealth args + extra args combined correctly."""
pw_cm, pw, context = _make_mock_pw_and_context()
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile", args=["--disable-gpu"])
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert "--disable-gpu" in call_kwargs["args"]
# Stealth args present by default
assert any(a.startswith("--fingerprint=") for a in call_kwargs["args"])
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
def test_persistent_context_default_viewport(_mock_geoip, _mock_bin):
"""DEFAULT_VIEWPORT applied when no viewport given."""
pw_cm, pw, context = _make_mock_pw_and_context()
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile")
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["viewport"] == DEFAULT_VIEWPORT
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
def test_persistent_context_custom_viewport(_mock_geoip, _mock_bin):
"""Custom viewport overrides DEFAULT_VIEWPORT."""
pw_cm, pw, context = _make_mock_pw_and_context()
custom = {"width": 1280, "height": 720}
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile", viewport=custom)
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["viewport"] == custom
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
def test_persistent_context_user_agent(_mock_geoip, _mock_bin):
"""user_agent forwarded to launch_persistent_context()."""
pw_cm, pw, context = _make_mock_pw_and_context()
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile", user_agent="Custom/1.0")
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["user_agent"] == "Custom/1.0"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
def test_persistent_context_locale_and_timezone(_mock_bin):
"""Both timezone and locale flow to context kwargs and binary args."""
pw_cm, pw, context = _make_mock_pw_and_context()
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile", timezone="Asia/Tokyo", locale="ja-JP")
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
# Context kwargs
assert call_kwargs["timezone_id"] == "Asia/Tokyo"
assert call_kwargs["locale"] == "ja-JP"
# Binary args
assert "--fingerprint-timezone=Asia/Tokyo" in call_kwargs["args"]
assert "--lang=ja-JP" in call_kwargs["args"]
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
def test_persistent_context_color_scheme(_mock_geoip, _mock_bin):
"""color_scheme forwarded correctly."""
pw_cm, pw, context = _make_mock_pw_and_context()
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile", color_scheme="dark")
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["color_scheme"] == "dark"
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=("Europe/Berlin", "de-DE"))
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
def test_persistent_context_geoip(_mock_bin, _mock_geoip):
"""geoip fills missing tz/locale."""
pw_cm, pw, context = _make_mock_pw_and_context()
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile", proxy="http://proxy:8080", geoip=True)
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["timezone_id"] == "Europe/Berlin"
assert call_kwargs["locale"] == "de-DE"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
def test_persistent_context_timezone_id_deprecation(_mock_bin):
"""Old timezone_id kwarg migrated with warning."""
pw_cm, pw, context = _make_mock_pw_and_context()
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
launch_persistent_context("/tmp/profile", timezone_id="Europe/Paris")
assert len(w) == 1
assert issubclass(w[0].category, FutureWarning)
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["timezone_id"] == "Europe/Paris"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
def test_persistent_context_close_stops_pw(_mock_geoip, _mock_bin):
"""context.close() also calls pw.stop()."""
pw_cm, pw, context = _make_mock_pw_and_context()
original_close = context.close
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
ctx = launch_persistent_context("/tmp/profile")
ctx.close()
original_close.assert_called_once()
pw.stop.assert_called_once()
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
def test_persistent_context_proxy_string(_mock_geoip, _mock_bin):
"""Proxy string parsed and passed."""
pw_cm, pw, context = _make_mock_pw_and_context()
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile", proxy="http://user:pass@proxy:8080")
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["proxy"]["server"] == "http://proxy:8080"
assert call_kwargs["proxy"]["username"] == "user"
assert call_kwargs["proxy"]["password"] == "pass"
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
def test_persistent_context_proxy_dict(_mock_geoip, _mock_bin):
"""Proxy dict passed through."""
pw_cm, pw, context = _make_mock_pw_and_context()
proxy_dict = {"server": "http://proxy:8080", "bypass": ".google.com"}
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context
launch_persistent_context("/tmp/profile", proxy=proxy_dict)
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["proxy"] == proxy_dict
# ---------------------------------------------------------------------------
# Async: launch_persistent_context_async()
# ---------------------------------------------------------------------------
def _make_mock_async_pw_and_context():
"""Create mock async_playwright chain returning a mock context."""
context = AsyncMock()
pw = AsyncMock()
pw.chromium.launch_persistent_context.return_value = context
pw_cm = AsyncMock()
pw_cm.start.return_value = pw
return pw_cm, pw, context
@pytest.mark.asyncio
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
async def test_persistent_context_async_args_built(_mock_geoip, _mock_bin):
"""Async launch builds args correctly."""
pw_cm, pw, context = _make_mock_async_pw_and_context()
with patch("patchright.async_api.async_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context_async
await launch_persistent_context_async("/tmp/profile", args=["--disable-gpu"])
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert "--disable-gpu" in call_kwargs["args"]
assert any(a.startswith("--fingerprint=") for a in call_kwargs["args"])
@pytest.mark.asyncio
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
@patch("cloakbrowser.browser._maybe_resolve_geoip", return_value=(None, None))
async def test_persistent_context_async_close_stops_pw(_mock_geoip, _mock_bin):
"""await context.close() calls await pw.stop()."""
pw_cm, pw, context = _make_mock_async_pw_and_context()
original_close = context.close
with patch("patchright.async_api.async_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context_async
ctx = await launch_persistent_context_async("/tmp/profile")
await ctx.close()
original_close.assert_called_once()
pw.stop.assert_called_once()
@pytest.mark.asyncio
@patch("cloakbrowser.browser.ensure_binary", return_value="/fake/chrome")
async def test_persistent_context_async_timezone_id_deprecation(_mock_bin):
"""Deprecated timezone_id kwarg migrated with warning in async path."""
pw_cm, pw, context = _make_mock_async_pw_and_context()
with patch("patchright.async_api.async_playwright", return_value=pw_cm):
from cloakbrowser.browser import launch_persistent_context_async
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
await launch_persistent_context_async("/tmp/profile", timezone_id="Europe/Paris")
assert len(w) == 1
assert issubclass(w[0].category, FutureWarning)
call_kwargs = pw.chromium.launch_persistent_context.call_args[1]
assert call_kwargs["timezone_id"] == "Europe/Paris"
+118
View File
@@ -24,6 +24,10 @@ from cloakbrowser.download import (
_parse_checksums,
_should_check_for_update,
_verify_checksum,
_write_version_marker,
check_for_update,
clear_cache,
ensure_binary,
)
@@ -356,3 +360,117 @@ class TestVerifyChecksum:
file.write_bytes(b"real content")
with pytest.raises(RuntimeError, match="Checksum verification failed"):
_verify_checksum(file, "0" * 64)
class TestClearCache:
def test_removes_dir(self, tmp_path):
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
# Create some content
(tmp_path / "chromium-145").mkdir()
(tmp_path / "chromium-145" / "chrome").write_bytes(b"binary")
clear_cache()
assert not tmp_path.exists()
def test_noop_if_missing(self, tmp_path):
nonexistent = tmp_path / "nonexistent"
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(nonexistent)}):
clear_cache() # Should not raise
class TestCheckForUpdate:
@patch("cloakbrowser.download._maybe_trigger_update_check")
def test_returns_none_when_current(self, _mock_update):
with patch("cloakbrowser.download._get_latest_chromium_version", return_value=None):
assert check_for_update() is None
@patch("cloakbrowser.download._maybe_trigger_update_check")
def test_returns_none_on_network_error(self, _mock_update):
with patch("cloakbrowser.download._get_latest_chromium_version", side_effect=Exception("timeout")):
# _get_latest_chromium_version catches exceptions internally, but
# check_for_update itself can also fail — test graceful None return
with patch("cloakbrowser.download._get_latest_chromium_version", return_value=None):
assert check_for_update() is None
@patch("cloakbrowser.download._maybe_trigger_update_check")
def test_returns_version_when_newer(self, _mock_update, tmp_path):
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
with patch("cloakbrowser.download._get_latest_chromium_version", return_value="999.0.0.0"):
with patch("cloakbrowser.download._download_and_extract"):
result = check_for_update()
assert result == "999.0.0.0"
@patch("cloakbrowser.download._maybe_trigger_update_check")
def test_skips_download_if_already_cached(self, _mock_update, tmp_path):
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
# Create the binary dir so it looks already downloaded
binary_dir = tmp_path / "chromium-999.0.0.0"
binary_dir.mkdir()
with patch("cloakbrowser.download._get_latest_chromium_version", return_value="999.0.0.0"):
with patch("cloakbrowser.download._download_and_extract") as mock_dl:
result = check_for_update()
assert result == "999.0.0.0"
mock_dl.assert_not_called()
class TestEnsureBinary:
@patch("cloakbrowser.download._maybe_trigger_update_check")
def test_local_override(self, _mock_update, tmp_path):
binary = tmp_path / "chrome"
binary.write_bytes(b"binary")
with patch.dict(os.environ, {"CLOAKBROWSER_BINARY_PATH": str(binary)}):
result = ensure_binary()
assert result == str(binary)
@patch("cloakbrowser.download._maybe_trigger_update_check")
def test_local_override_missing_file(self, _mock_update):
with patch.dict(os.environ, {"CLOAKBROWSER_BINARY_PATH": "/nonexistent/chrome"}):
with pytest.raises(FileNotFoundError, match="does not exist"):
ensure_binary()
@patch("cloakbrowser.download._maybe_trigger_update_check")
def test_cached_binary_found(self, _mock_update, tmp_path):
with patch.dict(os.environ, {
"CLOAKBROWSER_CACHE_DIR": str(tmp_path),
"CLOAKBROWSER_BINARY_PATH": "",
}):
# Create a fake cached binary
version = get_chromium_version()
with patch("cloakbrowser.download.get_binary_path") as mock_path:
fake_binary = tmp_path / "chrome"
fake_binary.write_bytes(b"binary")
fake_binary.chmod(0o755)
mock_path.return_value = fake_binary
with patch("cloakbrowser.download.check_platform_available"):
result = ensure_binary()
assert result == str(fake_binary)
@patch("cloakbrowser.download._maybe_trigger_update_check")
def test_downloads_when_missing(self, _mock_update, tmp_path):
with patch.dict(os.environ, {
"CLOAKBROWSER_CACHE_DIR": str(tmp_path),
"CLOAKBROWSER_BINARY_PATH": "",
}):
fake_binary = tmp_path / "chrome"
with patch("cloakbrowser.download.check_platform_available"):
with patch("cloakbrowser.download.get_binary_path") as mock_path:
# effective == platform_version (no marker), so fallback block skipped.
# Call 1: get_binary_path(effective) → nonexistent (triggers download)
# Call 2: get_binary_path() → fake_binary (post-download verify)
mock_path.side_effect = [
tmp_path / "nonexistent", # pre-download: not cached
fake_binary, # post-download: binary ready
]
with patch("cloakbrowser.download._download_and_extract") as mock_dl:
fake_binary.write_bytes(b"binary")
result = ensure_binary()
mock_dl.assert_called_once()
assert result == str(fake_binary)
class TestWriteVersionMarker:
def test_creates_file(self, tmp_path):
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
_write_version_marker("999.0.0.0")
marker = tmp_path / f"latest_version_{get_platform_tag()}"
assert marker.exists()
assert marker.read_text() == "999.0.0.0"