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:
CloakHQ
2026-02-27 09:20:56 +01:00
parent 8ffaf86abe
commit f480958ba7
2 changed files with 130 additions and 18 deletions
+2
View File
@@ -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`.
+127 -17
View File
@@ -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(8000)
results = page.evaluate("""() => {
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) page.wait_for_timeout(5000)
content = page.content() except Exception:
# The page shows bot detection results pass
assert "deviceandbrowserinfo" in page.url.lower()
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}"