void_crawl_core 0.4.1

Rust-native CDP browser automation core — stealth-patched headless Chrome, profile leasing, captcha detection
Documentation
#![allow(clippy::expect_used, clippy::unwrap_used, clippy::missing_panics_doc, clippy::panic)]

use std::{
    fs::{self, OpenOptions},
    process,
};

use fd_lock::RwLock as FileLock;
use void_crawl_core::{MAX_PROFILE_SPLIT_COPIES, ProfileRegistry, ProfileStatus, VoidCrawlError};

#[test]
fn registry_uses_env_root_and_creates_profile_metadata() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path());

    let created = registry
        .create_profile("google-001", Some("Google SERP profile".into()), vec!["serp".into()])
        .unwrap();

    assert_eq!(created.profile.id, "google-001");
    assert_eq!(created.profile.description.as_deref(), Some("Google SERP profile"));
    assert_eq!(created.profile.labels, vec!["serp"]);
    assert!(created.profile.path.ends_with("google-001"));
    assert!(created.profile.path.join("Default").join("Preferences").is_file());
    assert_eq!(created.status, ProfileStatus::Available);

    let listed = registry.list_profiles().unwrap();
    assert_eq!(listed.len(), 1);
    assert_eq!(listed[0].profile.id, "google-001");
}

#[test]
fn delete_rejects_locked_profile() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path());
    let profile = registry.create_profile("busy", None, vec![]).unwrap();
    let lock_path = profile.profile.path.join(".voidcrawl.lock");
    let file = OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .truncate(false)
        .open(lock_path)
        .unwrap();
    let mut lock = FileLock::new(file);
    let _guard = lock.try_write().unwrap();

    let err = registry.delete_profile("busy").unwrap_err();
    assert!(matches!(err, VoidCrawlError::ProfileBusy { .. }));
}

#[test]
fn pool_validation_and_resolution_preserve_order() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path());
    registry.create_profile("a", None, vec![]).unwrap();
    registry.create_profile("b", None, vec![]).unwrap();

    let pool = registry.create_pool("google-serp", vec!["a".into(), "b".into()], 3).unwrap();
    assert_eq!(pool.profile_ids, vec!["a", "b"]);
    assert_eq!(pool.max_active, 3);

    let resolved = registry.resolve_pool("google-serp").unwrap();
    let ids: Vec<_> = resolved.profiles.into_iter().map(|p| p.profile.id).collect();
    assert_eq!(ids, vec!["a", "b"]);

    let err = registry.create_pool("bad", vec!["missing".into()], 3).unwrap_err();
    assert!(matches!(err, VoidCrawlError::ProfileNotFound { .. }));
}

#[test]
fn busy_profile_reports_voidcrawl_owner_metadata() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path());
    registry.create_profile("owned", None, vec![]).unwrap();
    let _lease = registry.acquire_profile("owned").unwrap();

    let error = registry.acquire_profile("owned").unwrap_err();
    match error {
        VoidCrawlError::ProfileBusy { pid, acquired_at, .. } => {
            assert_eq!(pid, Some(process::id()));
            assert!(acquired_at.is_some());
        }
        other => panic!("expected ProfileBusy, got {other:?}"),
    }
}

#[test]
fn snapshot_is_isolated_and_cleans_up_on_drop() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path());
    let profile = registry.create_profile("source", None, vec![]).unwrap();
    fs::write(profile.profile.path.join("Default").join("Cookies"), "cookie-data").unwrap();
    fs::write(profile.profile.path.join("SingletonLock"), "transient").unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::symlink;
        let outside = tmp.path().join("outside-secret");
        fs::write(&outside, "must-not-be-copied").unwrap();
        symlink(outside, profile.profile.path.join("ExternalLink")).unwrap();
    }

    let snapshot = registry.snapshot_profile("source").unwrap();
    let snapshot_path = snapshot.path().to_path_buf();
    assert_eq!(
        fs::read_to_string(snapshot_path.join("Default").join("Cookies")).unwrap(),
        "cookie-data"
    );
    assert!(!snapshot_path.join(".voidcrawl.lock").exists());
    assert!(!snapshot_path.join("SingletonLock").exists());
    #[cfg(unix)]
    assert!(!snapshot_path.join("ExternalLink").exists());
    drop(snapshot);
    assert!(!snapshot_path.exists());
}

#[test]
fn split_profile_creates_same_baseline_in_unique_cleanup_scoped_directories() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path());
    let profile = registry.create_profile("source", None, vec![]).unwrap();
    fs::write(profile.profile.path.join("Default").join("Cookies"), "shared-baseline").unwrap();

    let split = registry.split_profile("source", 2).unwrap();
    let paths = split.iter().map(|snapshot| snapshot.path().to_path_buf()).collect::<Vec<_>>();
    assert_ne!(paths[0], paths[1]);
    for path in &paths {
        assert_eq!(
            fs::read_to_string(path.join("Default").join("Cookies")).unwrap(),
            "shared-baseline"
        );
        assert!(!path.join(".voidcrawl.lock").exists());
    }

    drop(split);
    assert!(paths.iter().all(|path| !path.exists()));
}

#[test]
fn fork_native_profile_normalizes_default_and_copies_local_state() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path().join("registry"));
    let user_data = tmp.path().join("chrome-user-data");
    let native = user_data.join("Profile 7");
    fs::create_dir_all(&native).unwrap();
    fs::write(native.join("Preferences"), r#"{"profile":{"name":"Work"}}"#).unwrap();
    fs::write(native.join("Cookies"), "authenticated-state").unwrap();
    fs::write(user_data.join("Local State"), "encryption-context").unwrap();

    let forks = registry.fork_profile(native.to_str().unwrap(), 2).unwrap();
    let paths = forks.iter().map(|fork| fork.path().to_path_buf()).collect::<Vec<_>>();
    assert_ne!(paths[0], paths[1]);
    for path in &paths {
        assert_eq!(
            fs::read_to_string(path.join("Default").join("Cookies")).unwrap(),
            "authenticated-state"
        );
        assert_eq!(fs::read_to_string(path.join("Local State")).unwrap(), "encryption-context");
        assert!(!path.join("Profile 7").exists());
    }

    drop(forks);
    assert!(paths.iter().all(|path| !path.exists()));
}

#[test]
fn fork_native_profile_rejects_a_running_chrome_root() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path().join("registry"));
    let user_data = tmp.path().join("chrome-user-data");
    let native = user_data.join("Default");
    fs::create_dir_all(&native).unwrap();
    fs::write(native.join("Preferences"), "{}").unwrap();
    fs::write(user_data.join("SingletonLock"), "active").unwrap();

    let error = registry.fork_profile(native.to_str().unwrap(), 2).unwrap_err();
    assert!(matches!(error, VoidCrawlError::ChromeProfileBusy { .. }));
}

#[test]
fn split_profile_rejects_unbounded_copy_counts() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path());
    registry.create_profile("source", None, vec![]).unwrap();

    assert!(registry.split_profile("source", 1).is_err());
    assert!(registry.split_profile("source", MAX_PROFILE_SPLIT_COPIES + 1).is_err());
}

#[test]
fn pool_leasing_respects_active_cap() {
    let tmp = tempfile::tempdir().unwrap();
    let registry = ProfileRegistry::new(tmp.path());
    registry.create_profile("a", None, vec![]).unwrap();
    registry.create_profile("b", None, vec![]).unwrap();
    registry.create_pool("single", vec!["a".into(), "b".into()], 1).unwrap();

    let first = registry.acquire_from_pool("single").unwrap();
    assert_eq!(first.id(), "a");
    let second = registry.acquire_from_pool("single").unwrap();
    assert_eq!(second.id(), "b");

    let err = registry.acquire_from_pool("single").unwrap_err();
    assert!(matches!(err, VoidCrawlError::ProfileBusy { .. }));
}