webpuppet 0.1.5-alpha

Web browser programmatic automation and control library for research, testing, and workflow automation
Documentation
//! Integration tests for webpuppet.
//!
//! These tests require a browser to be installed and optionally authenticated.

use webpuppet::{
    BrowserDetector, ContentScreener, InterventionDetector, InterventionReason, Operation,
    PermissionGuard, PermissionPolicy,
};

// ============================================================================
// Browser Detection Tests
// ============================================================================

#[test]
fn test_browser_detection_finds_brave() {
    let browsers = BrowserDetector::detect_all();

    // At least one browser should be detected
    assert!(
        !browsers.is_empty(),
        "No browsers detected. Install Brave, Chrome, or Chromium."
    );

    // Print what was found
    for browser in &browsers {
        println!(
            "Found: {:?} at {} (version: {})",
            browser.browser_type,
            browser.executable_path.display(),
            browser.version.as_deref().unwrap_or("unknown")
        );
    }
}

#[test]
fn test_brave_specific_detection() {
    let brave = BrowserDetector::detect_brave();

    if let Some(installation) = brave {
        println!("Brave found at: {}", installation.executable_path.display());
        println!(
            "Version: {}",
            installation.version.as_deref().unwrap_or("unknown")
        );
        println!("User data: {}", installation.user_data_dir.display());

        // Check profiles
        if let Ok(profiles) = installation.list_profiles() {
            println!("Profiles: {:?}", profiles);
        }

        // The executable should exist
        assert!(installation.executable_path.exists());
        assert!(installation.user_data_dir.exists());
    } else {
        println!("Brave not installed, skipping specific test");
    }
}

#[test]
fn test_browser_installation_validity() {
    let browsers = BrowserDetector::detect_all();

    for browser in &browsers {
        assert!(
            browser.is_valid(),
            "{:?} should be valid",
            browser.browser_type
        );
        println!(
            "{:?} is valid: executable exists at {}",
            browser.browser_type,
            browser.executable_path.display()
        );
    }
}

// ============================================================================
// Permission System Tests
// ============================================================================

#[test]
fn test_secure_policy_blocks_destructive_ops() {
    let guard = PermissionGuard::new(PermissionPolicy::secure());

    // Safe operations should be allowed
    assert!(guard.check(Operation::ReadContent).allowed);
    assert!(guard.check(Operation::Navigate).allowed);
    assert!(guard.check(Operation::SendPrompt).allowed);

    // Destructive operations should be blocked
    assert!(!guard.check(Operation::DeleteAccount).allowed);
    assert!(!guard.check(Operation::ChangePassword).allowed);
    assert!(!guard.check(Operation::ModifyAccountSettings).allowed);
    assert!(!guard.check(Operation::RevokeTokens).allowed);
}

#[test]
fn test_readonly_policy_blocks_all_writes() {
    let guard = PermissionGuard::new(PermissionPolicy::read_only());

    // Read operations should be allowed
    assert!(guard.check(Operation::ReadContent).allowed);
    assert!(guard.check(Operation::Navigate).allowed);
    assert!(guard.check(Operation::Screenshot).allowed);
    assert!(guard.check(Operation::ReadResponse).allowed);

    // Write operations should be blocked
    assert!(!guard.check(Operation::Click).allowed);
    assert!(!guard.check(Operation::TypeText).allowed);
    assert!(!guard.check(Operation::SendPrompt).allowed);
    assert!(!guard.check(Operation::SubmitForm).allowed);
}

#[test]
fn test_url_domain_restrictions() {
    let guard = PermissionGuard::new(PermissionPolicy::secure());

    // Allowed domains
    assert!(
        guard
            .check_with_url(Operation::Navigate, "https://claude.ai/chat")
            .allowed
    );
    assert!(
        guard
            .check_with_url(Operation::Navigate, "https://x.com/i/grok")
            .allowed
    );
    assert!(
        guard
            .check_with_url(Operation::Navigate, "https://gemini.google.com")
            .allowed
    );

    // Restricted domains
    assert!(
        !guard
            .check_with_url(Operation::Navigate, "https://evil.com/phishing")
            .allowed
    );
    assert!(
        !guard
            .check_with_url(Operation::Navigate, "https://random-site.com")
            .allowed
    );
}

#[test]
fn test_risk_levels() {
    let guard = PermissionGuard::new(PermissionPolicy::secure());

    // Low risk operations
    let read_decision = guard.check(Operation::ReadContent);
    assert!(read_decision.risk_level <= 2);

    // High risk operations
    let delete_decision = guard.check(Operation::DeleteAccount);
    assert!(delete_decision.risk_level >= 8);

    // Medium risk operations
    let prompt_decision = guard.check(Operation::SendPrompt);
    assert!(prompt_decision.risk_level >= 2);
    assert!(prompt_decision.risk_level <= 5);
}

#[test]
fn test_audit_logging() {
    let guard = PermissionGuard::new(PermissionPolicy::secure());

    // Perform some operations
    guard.check(Operation::Navigate);
    guard.check(Operation::SendPrompt);
    guard.check(Operation::DeleteAccount);

    // Get audit log
    let logs = guard.audit_log();
    assert!(logs.len() >= 3);

    // Check log entries
    for entry in logs {
        println!(
            "[{}] {:?}: {} (allowed: {})",
            entry.timestamp.format("%H:%M:%S"),
            entry.operation,
            entry.reason,
            entry.allowed
        );
    }
}

// ============================================================================
// Content Screening Tests
// ============================================================================

#[test]
fn test_safe_content_passes() {
    let screener = ContentScreener::new();

    let safe_content = "Here's how to implement a binary search in Rust:
```rust
fn binary_search(arr: &[i32], target: i32) -> Option<usize> {
    let mut low = 0;
    let mut high = arr.len();
    while low < high {
        let mid = (low + high) / 2;
        if arr[mid] < target {
            low = mid + 1;
        } else if arr[mid] > target {
            high = mid;
        } else {
            return Some(mid);
        }
    }
    None
}
```";

    let result = screener.screen(safe_content);
    assert!(
        result.passed,
        "Safe content should pass: {:?}",
        result.issues
    );
}

#[test]
fn test_dangerous_content_flagged() {
    let screener = ContentScreener::new();

    // Content with zero-width characters (invisible text attack)
    let suspicious_content = "Hello\u{200B}\u{200C}\u{200D}world"; // Zero-width space, non-joiner, joiner

    let result = screener.screen(suspicious_content);
    println!("Risk score: {}", result.risk_score);
    println!("Issues: {:?}", result.issues);

    // Should have some issues for zero-width chars
    // If detection is working, it should find them
    // Note: May pass if screener doesn't flag these specific patterns
}

// ============================================================================
// Intervention Detection Tests
// ============================================================================

#[test]
fn test_captcha_detection_patterns() {
    let detector = InterventionDetector::new();

    // Test cases using the actual patterns in the detector (case-insensitive contains)
    let test_cases = [
        // Should detect - contains "recaptcha"
        (
            r#"<iframe src="https://www.google.com/recaptcha/api2/anchor"></iframe>"#,
            true,
        ),
        // Should detect - contains "hcaptcha"
        (r#"<div class="h-captcha" data-sitekey="abc"></div>"#, true),
        // Should detect - contains "cloudflare"
        (
            r#"<iframe src="https://challenges.cloudflare.com/"></iframe>"#,
            true,
        ),
        // Should NOT detect
        (r#"<form><input type="text" name="email"></form>"#, false),
    ];

    for (html, should_detect) in test_cases {
        let result = detector.detect_captcha(html);
        if should_detect {
            assert!(result.is_some(), "Should detect captcha in: {}", html);
        } else {
            assert!(result.is_none(), "Should not detect captcha in: {}", html);
        }
    }
}

#[test]
fn test_2fa_detection_patterns() {
    let detector = InterventionDetector::new();

    // Test cases - detector uses lowercase contains matching
    let test_cases = [
        // Contains "two-factor"
        (
            r#"<div class="two-factor-auth">Enter your code</div>"#,
            true,
        ),
        // Contains "verification code"
        (r#"<input placeholder="Enter verification code">"#, true),
        // Contains "one-time"
        (r#"<input autocomplete="one-time-code" type="text">"#, true),
        // Should NOT detect
        (r#"<input name="username" type="text">"#, false),
    ];

    for (html, should_detect) in test_cases {
        let result = detector.detect_2fa(html);
        if should_detect {
            assert!(result.is_some(), "Should detect 2FA in: {}", html);
        } else {
            assert!(result.is_none(), "Should not detect 2FA in: {}", html);
        }
    }
}

#[test]
fn test_rate_limit_detection() {
    let detector = InterventionDetector::new();

    let limited_html = "<div>Too many requests. Please try again in 30 seconds.</div>";
    let result = detector.detect_rate_limit(limited_html);

    assert!(result.is_some());
    if let Some(InterventionReason::RateLimited { wait_seconds }) = result {
        assert_eq!(wait_seconds, Some(30));
    }
}

#[test]
fn test_login_detection() {
    let detector = InterventionDetector::new();

    // URL-based detection
    let result = detector.detect_login_required("", "https://example.com/login");
    assert!(result.is_some());

    // Content-based detection
    let html = "<div>Please sign in to continue</div>";
    let result = detector.detect_login_required(html, "https://example.com/dashboard");
    assert!(result.is_some());
}

// ============================================================================
// Integration: Permission + Screening
// ============================================================================

#[test]
fn test_permission_and_screening_integration() {
    let guard = PermissionGuard::new(PermissionPolicy::secure());
    let screener = ContentScreener::new();

    // Simulate a workflow:
    // 1. Check permission to send prompt
    let can_prompt = guard.require(Operation::SendPrompt);
    assert!(can_prompt.is_ok(), "Should be able to send prompt");

    // 2. Simulate receiving a response
    let response = "The answer to your question about Rust async is...";

    // 3. Screen the response
    let screening = screener.screen(response);
    assert!(screening.passed, "Response should pass screening");

    // 4. Audit trail
    let logs = guard.audit_log();
    assert!(!logs.is_empty(), "Should have audit entries");
}

// ============================================================================
// Edge Cases
// ============================================================================

#[test]
fn test_empty_inputs() {
    let screener = ContentScreener::new();
    let result = screener.screen("");
    assert!(result.passed, "Empty content should pass");

    let detector = InterventionDetector::new();
    assert!(detector.detect_captcha("").is_none());
    assert!(detector.detect_2fa("").is_none());
}

#[test]
fn test_unicode_handling() {
    let screener = ContentScreener::new();

    let unicode_content = "这是中文内容 🦀 Rust は素晴らしい言語です émojis: 🎉🎊";
    let result = screener.screen(unicode_content);
    assert!(result.passed, "Unicode content should pass");
}

#[test]
fn test_large_content() {
    let screener = ContentScreener::new();

    // Generate large but safe content
    let large_content = "Hello world. ".repeat(10000);
    let result = screener.screen(&large_content);
    assert!(result.passed, "Large safe content should pass");
}