mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
test: add real bot detection assertions to stealth tests
Replace hollow page-load checks with actual verdict parsing for 6 detection sites: sannysoft, incolumitas, browserscan, deviceandbrowserinfo, fingerprintjs, and recaptcha v3. Add proxy support via CLOAKBROWSER_TEST_PROXY env var. All 12 stealth tests pass.
This commit is contained in:
@@ -403,6 +403,8 @@ page.goto("https://example.com")
|
|||||||
| macOS x86_64 (Intel) | ✅ Available |
|
| macOS x86_64 (Intel) | ✅ Available |
|
||||||
| Windows | Planned |
|
| Windows | Planned |
|
||||||
|
|
||||||
|
**macOS (early access):** macOS builds are new — tested but not yet battle-tested at scale like Linux. If you hit any issues, [please open a GitHub issue](https://github.com/CloakHQ/CloakBrowser/issues).
|
||||||
|
|
||||||
**macOS first launch:** The binary is ad-hoc signed. On first run, macOS Gatekeeper will block it. Right-click the app → **Open** → click **Open** in the dialog. This is only needed once.
|
**macOS first launch:** The binary is ad-hoc signed. On first run, macOS Gatekeeper will block it. Right-click the app → **Open** → click **Open** in the dialog. This is only needed once.
|
||||||
|
|
||||||
**On Windows?** You can still use CloakBrowser via Docker or with your own Chromium binary by setting `CLOAKBROWSER_BINARY_PATH=/path/to/chrome`.
|
**On Windows?** You can still use CloakBrowser via Docker or with your own Chromium binary by setting `CLOAKBROWSER_BINARY_PATH=/path/to/chrome`.
|
||||||
|
|||||||
+128
-18
@@ -4,14 +4,18 @@ These tests verify that the stealth Chromium binary passes common
|
|||||||
bot detection checks. They require network access.
|
bot detection checks. They require network access.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from cloakbrowser import launch
|
from cloakbrowser import launch
|
||||||
|
|
||||||
|
PROXY = os.environ.get("CLOAKBROWSER_TEST_PROXY")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def browser():
|
def browser():
|
||||||
"""Shared browser instance for stealth tests."""
|
"""Shared browser instance for stealth tests."""
|
||||||
b = launch(headless=True)
|
b = launch(headless=True, proxy=PROXY)
|
||||||
yield b
|
yield b
|
||||||
b.close()
|
b.close()
|
||||||
|
|
||||||
@@ -48,7 +52,7 @@ class TestWebDriverDetection:
|
|||||||
"""Must have browser plugins (real Chrome has 5)."""
|
"""Must have browser plugins (real Chrome has 5)."""
|
||||||
page.goto("https://example.com")
|
page.goto("https://example.com")
|
||||||
count = page.evaluate("navigator.plugins.length")
|
count = page.evaluate("navigator.plugins.length")
|
||||||
assert count >= 1, f"Expected plugins, got {count}"
|
assert count >= 5, f"Expected 5+ plugins (real Chrome), got {count}"
|
||||||
|
|
||||||
def test_languages_present(self, page):
|
def test_languages_present(self, page):
|
||||||
"""navigator.languages must be populated."""
|
"""navigator.languages must be populated."""
|
||||||
@@ -81,28 +85,134 @@ class TestBotDetectionSites:
|
|||||||
Mark with pytest -m slow to skip in CI.
|
Mark with pytest -m slow to skip in CI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_bot_sannysoft(self, page):
|
||||||
|
"""bot.sannysoft.com — all checks should pass (0 failures)."""
|
||||||
|
page.goto("https://bot.sannysoft.com", wait_until="networkidle", timeout=30000)
|
||||||
|
page.wait_for_timeout(3000)
|
||||||
|
|
||||||
|
results = page.evaluate("""() => {
|
||||||
|
const rows = document.querySelectorAll('table tr');
|
||||||
|
const failed = [];
|
||||||
|
let total = 0;
|
||||||
|
rows.forEach(r => {
|
||||||
|
const cells = r.querySelectorAll('td');
|
||||||
|
if (cells.length >= 2) {
|
||||||
|
total++;
|
||||||
|
const cls = cells[1].className || '';
|
||||||
|
if (cls.includes('failed')) {
|
||||||
|
failed.push(cells[0].innerText.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {total, failed};
|
||||||
|
}""")
|
||||||
|
|
||||||
|
failed = results["failed"]
|
||||||
|
assert len(failed) == 0, f"Sannysoft failures: {', '.join(failed)}"
|
||||||
|
|
||||||
@pytest.mark.slow
|
@pytest.mark.slow
|
||||||
def test_bot_incolumitas(self, page):
|
def test_bot_incolumitas(self, page):
|
||||||
"""bot.incolumitas.com should detect minimal flags."""
|
"""bot.incolumitas.com — max 1 failure (WEBDRIVER false positive expected)."""
|
||||||
page.goto("https://bot.incolumitas.com", timeout=30000)
|
page.goto("https://bot.incolumitas.com", wait_until="networkidle", timeout=30000)
|
||||||
page.wait_for_timeout(5000)
|
page.wait_for_timeout(12000)
|
||||||
# Check that we're not immediately flagged
|
|
||||||
title = page.title()
|
# Known acceptable failures (not browser fingerprint issues):
|
||||||
assert title # Page loaded successfully
|
# - WEBDRIVER: spec-level false positive across all builds
|
||||||
|
# - connectionRTT: detects datacenter/proxy network latency, not browser
|
||||||
|
KNOWN_ACCEPTABLE = {"WEBDRIVER", "connectionRTT"}
|
||||||
|
|
||||||
|
results = page.evaluate("""() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const okMatches = text.match(/"\\w+":\\s*"OK"/g) || [];
|
||||||
|
const failMatches = text.match(/"\\w+":\\s*"FAIL"/g) || [];
|
||||||
|
const failedTests = failMatches.map(m => m.match(/"(\\w+)"/)[1]);
|
||||||
|
return {passed: okMatches.length, failed: failMatches.length, failedTests};
|
||||||
|
}""")
|
||||||
|
|
||||||
|
failed_names = results["failedTests"]
|
||||||
|
real_failures = [f for f in failed_names if f not in KNOWN_ACCEPTABLE]
|
||||||
|
assert len(real_failures) == 0, f"Incolumitas unexpected failures: {', '.join(real_failures)}"
|
||||||
|
|
||||||
@pytest.mark.slow
|
@pytest.mark.slow
|
||||||
def test_browserscan(self, page):
|
def test_browserscan(self, page):
|
||||||
"""BrowserScan bot detection should show NORMAL."""
|
"""BrowserScan bot detection — 0 abnormal checks."""
|
||||||
page.goto("https://www.browserscan.net/bot-detection", timeout=30000)
|
page.goto("https://www.browserscan.net/bot-detection", wait_until="networkidle", timeout=30000)
|
||||||
page.wait_for_timeout(5000)
|
page.wait_for_timeout(5000)
|
||||||
title = page.title()
|
|
||||||
assert title # Page loaded
|
results = page.evaluate("""() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const normalMatches = text.match(/Normal/g);
|
||||||
|
const abnormalMatches = text.match(/Abnormal/g);
|
||||||
|
return {
|
||||||
|
normal: normalMatches ? normalMatches.length : 0,
|
||||||
|
abnormal: abnormalMatches ? abnormalMatches.length : 0
|
||||||
|
};
|
||||||
|
}""")
|
||||||
|
|
||||||
|
assert results["abnormal"] == 0, \
|
||||||
|
f"BrowserScan: {results['abnormal']} abnormal, {results['normal']} normal"
|
||||||
|
|
||||||
@pytest.mark.slow
|
@pytest.mark.slow
|
||||||
def test_device_and_browser_info(self, page):
|
def test_device_and_browser_info(self, page):
|
||||||
"""deviceandbrowserinfo.com should report isBot: false."""
|
"""deviceandbrowserinfo.com — isBot must be false."""
|
||||||
page.goto("https://deviceandbrowserinfo.com/are_you_a_bot", timeout=30000)
|
page.goto("https://deviceandbrowserinfo.com/are_you_a_bot", wait_until="domcontentloaded", timeout=30000)
|
||||||
page.wait_for_timeout(5000)
|
page.wait_for_timeout(8000)
|
||||||
content = page.content()
|
|
||||||
# The page shows bot detection results
|
results = page.evaluate("""() => {
|
||||||
assert "deviceandbrowserinfo" in page.url.lower()
|
const text = document.body.innerText;
|
||||||
|
const botMatch = text.match(/"isBot":\\s*(true|false)/);
|
||||||
|
const isBot = botMatch ? botMatch[1] === 'true' : null;
|
||||||
|
const checks = {};
|
||||||
|
['isBot', 'hasBotUserAgent', 'hasWebdriverTrue', 'isHeadlessChrome',
|
||||||
|
'isAutomatedWithCDP', 'hasSuspiciousWeakSignals', 'isPlaywright',
|
||||||
|
'hasInconsistentChromeObject'].forEach(p => {
|
||||||
|
const match = text.match(new RegExp('"' + p + '":\\s*(true|false)'));
|
||||||
|
if (match) checks[p] = match[1] === 'true';
|
||||||
|
});
|
||||||
|
return {isBot, checks};
|
||||||
|
}""")
|
||||||
|
|
||||||
|
assert results["isBot"] is False, f"Detected as bot! Checks: {results['checks']}"
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_fingerprintjs(self, page):
|
||||||
|
"""FingerprintJS — must not be blocked, should see flight data."""
|
||||||
|
page.goto("https://demo.fingerprint.com/web-scraping", wait_until="domcontentloaded", timeout=30000)
|
||||||
|
page.wait_for_timeout(8000)
|
||||||
|
|
||||||
|
try:
|
||||||
|
page.click("button:has-text('Search')", timeout=5000)
|
||||||
|
page.wait_for_timeout(5000)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
results = page.evaluate("""() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const hasFlights = text.includes('Price per adult') || text.includes('$');
|
||||||
|
const isBlocked = text.includes('request was blocked') || text.includes('bot visit detected');
|
||||||
|
return {passed: hasFlights && !isBlocked, isBlocked, hasFlights};
|
||||||
|
}""")
|
||||||
|
|
||||||
|
assert not results["isBlocked"], "FingerprintJS blocked us as a bot"
|
||||||
|
assert results["passed"], "FingerprintJS: no flight data shown"
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_recaptcha_v3(self, page):
|
||||||
|
"""reCAPTCHA v3 — score must be >= 0.7."""
|
||||||
|
page.goto(
|
||||||
|
"https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php",
|
||||||
|
wait_until="domcontentloaded",
|
||||||
|
timeout=60000,
|
||||||
|
)
|
||||||
|
page.wait_for_timeout(8000)
|
||||||
|
|
||||||
|
results = page.evaluate("""() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const scoreMatch = text.match(/"score":\\s*(\\d+\\.\\d+)/);
|
||||||
|
return {score: scoreMatch ? parseFloat(scoreMatch[1]) : null};
|
||||||
|
}""")
|
||||||
|
|
||||||
|
score = results["score"]
|
||||||
|
assert score is not None, "Could not extract reCAPTCHA score"
|
||||||
|
assert score >= 0.7, f"reCAPTCHA score too low: {score}"
|
||||||
|
|||||||
Reference in New Issue
Block a user